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];
385 # endif // USE_IPHONE
389 xsft->setup_cb (xsft, xsft->setup_arg);
391 /* The plist files for these preferences show up in
392 $HOME/Library/Preferences/ByHost/ in a file named like
393 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
395 NSString *name = [NSString stringWithCString:xsft->progclass
396 encoding:NSISOLatin1StringEncoding];
397 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
398 [self setResourcesEnv:name];
401 XrmOptionDescRec *opts = 0;
402 const char **defs = 0;
403 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
404 prefsReader = [[PrefsReader alloc]
405 initWithName:name xrmKeys:opts defaults:defs];
407 // free (opts); // bah, we need these! #### leak!
408 xsft->options = opts;
410 progname = progclass = xsft->progclass;
414 # ifdef USE_BACKBUFFER
415 [self createBackbuffer];
420 // So we can tell when we're docked.
421 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
422 # endif // USE_IPHONE
429 # if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER)
430 [self setLayer: [CALayer layer]];
431 self.layer.delegate = self;
432 self.layer.opaque = YES;
433 [self setWantsLayer: YES];
434 # endif // !USE_IPHONE && BACKBUFFER_CALAYER
438 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
440 return [self initWithFrame:frame saverName:0 isPreview:p];
446 NSAssert(![self isAnimating], @"still animating");
447 NSAssert(!xdata, @"xdata not yet freed");
448 NSAssert(!xdpy, @"xdpy not yet freed");
450 # ifdef USE_BACKBUFFER
452 CGContextRelease (backbuffer);
455 CGColorSpaceRelease (colorspace);
457 # ifdef BACKBUFFER_CGCONTEXT
459 CGContextRelease (window_ctx);
460 # endif // BACKBUFFER_CGCONTEXT
462 # endif // USE_BACKBUFFER
464 [prefsReader release];
472 - (PrefsReader *) prefsReader
479 - (void) lockFocus { }
480 - (void) unlockFocus { }
486 /* A few seconds after the saver launches, we store the "wasRunning"
487 preference. This is so that if the saver is crashing at startup,
488 we don't launch it again next time, getting stuck in a crash loop.
490 - (void) allSystemsGo: (NSTimer *) timer
492 NSAssert (timer == crash_timer, @"crash timer screwed up");
495 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
496 [prefs setBool:YES forKey:@"wasRunning"];
502 - (void) startAnimation
504 NSAssert(![self isAnimating], @"already animating");
505 NSAssert(!initted_p && !xdata, @"already initialized");
507 // See comment in render_x11() for why this value is important:
508 [self setAnimationTimeInterval: 1.0 / 120.0];
510 [super startAnimation];
511 /* We can't draw on the window from this method, so we actually do the
512 initialization of the screen saver (xsft->init_cb) in the first call
513 to animateOneFrame() instead.
518 [crash_timer invalidate];
520 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
521 [prefs removeObjectForKey:@"wasRunning"];
524 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
526 selector:@selector(allSystemsGo:)
530 # endif // USE_IPHONE
532 // Never automatically turn the screen off if we are docked,
533 // and an animation is running.
536 [UIApplication sharedApplication].idleTimerDisabled =
537 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
538 [[UIApplication sharedApplication]
539 setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
544 - (void)stopAnimation
546 NSAssert([self isAnimating], @"not animating");
550 [self lockFocus]; // in case something tries to draw from here
551 [self prepareContext];
553 /* I considered just not even calling the free callback at all...
554 But webcollage-cocoa needs it, to kill the inferior webcollage
555 processes (since the screen saver framework never generates a
556 SIGPIPE for them...) Instead, I turned off the free call in
557 xlockmore.c, which is where all of the bogus calls are anyway.
559 xsft->free_cb (xdpy, xwindow, xdata);
562 // xdpy must be freed before dealloc is called, because xdpy owns a
563 // circular reference to the parent XScreenSaverView.
564 jwxyz_free_display (xdpy);
568 // setup_p = NO; // #### wait, do we need this?
575 [crash_timer invalidate];
577 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
578 [prefs removeObjectForKey:@"wasRunning"];
580 # endif // USE_IPHONE
582 [super stopAnimation];
584 // When an animation is no longer running (e.g., looking at the list)
585 // then it's ok to power off the screen when docked.
588 [UIApplication sharedApplication].idleTimerDisabled = NO;
589 [[UIApplication sharedApplication]
590 setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
595 /* Hook for the XScreenSaverGLView subclass
597 - (void) prepareContext
601 /* Hook for the XScreenSaverGLView subclass
603 - (void) resizeContext
609 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
611 fps_compute (fpst, 0, -1);
618 /* On iPhones with Retina displays, we can draw the savers in "real"
619 pixels, and that works great. The 320x480 "point" screen is really
620 a 640x960 *pixel* screen. However, Retina iPads have 768x1024
621 point screens which are 1536x2048 pixels, and apparently that's
622 enough pixels that copying those bits to the screen is slow. Like,
623 drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
624 real pixels. This will probably make the savers look better
625 anyway, since that's a higher resolution than most desktop monitors
626 have even today. (This is only true for X11 programs, not GL
627 programs. Those are fine at full rez.)
629 This method is overridden in XScreenSaverGLView, since this kludge
630 isn't necessary for GL programs, being resolution independent by
633 - (CGFloat) hackedContentScaleFactor
635 GLfloat s = [self contentScaleFactor];
636 if (initial_bounds.width >= 1024 ||
637 initial_bounds.height >= 1024)
643 static GLfloat _global_rot_current_angle_kludge;
645 double current_device_rotation (void)
647 return -_global_rot_current_angle_kludge;
651 - (void) hackRotation
653 if (rotation_ratio >= 0) { // in the midst of a rotation animation
655 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
656 GLfloat f = angle_from;
657 GLfloat t = angle_to;
660 GLfloat dist = -(t-f);
663 // Intermediate angle.
664 rot_current_angle = f - rotation_ratio * dist;
666 // Intermediate frame size.
667 rot_current_size.width = rot_from.width +
668 rotation_ratio * (rot_to.width - rot_from.width);
669 rot_current_size.height = rot_from.height +
670 rotation_ratio * (rot_to.height - rot_from.height);
672 // Tick animation. Complete rotation in 1/6th sec.
673 double now = double_time();
674 double duration = 1/6.0;
675 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
677 if (rotation_ratio > 1) { // Done animating.
678 orientation = new_orientation;
679 rot_current_angle = angle_to;
680 rot_current_size = rot_to;
683 // Check orientation again in case we rotated again while rotating:
684 // this is a no-op if nothing has changed.
685 [self didRotate:nil];
687 } else { // Not animating a rotation.
688 rot_current_angle = angle_to;
689 rot_current_size = rot_to;
692 CLAMP180(rot_current_angle);
693 _global_rot_current_angle_kludge = rot_current_angle;
697 double s = [self hackedContentScaleFactor];
698 if (!ignore_rotation_p &&
699 /* rotation_ratio && */
700 ((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
701 (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
706 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
708 if (i == 0) exit (-1); // Cancel
709 [self stopAndClose:NO]; // Keep going
712 - (void) handleException: (NSException *)e
714 NSLog (@"Caught exception: %@", e);
715 [[[UIAlertView alloc] initWithTitle:
716 [NSString stringWithFormat: @"%s crashed!",
719 [NSString stringWithFormat:
720 @"The error message was:"
722 "If it keeps crashing, try "
723 "resetting its options.",
726 cancelButtonTitle: @"Exit"
727 otherButtonTitles: @"Keep going", nil]
729 [self stopAnimation];
735 #ifdef USE_BACKBUFFER
737 /* Create a bitmap context into which we render everything.
738 If the desired size has changed, re-created it.
740 - (void) createBackbuffer
743 double s = [self hackedContentScaleFactor];
744 CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size;
745 int new_w = s * rotsize.width;
746 int new_h = s * rotsize.height;
748 int new_w = [self bounds].size.width;
749 int new_h = [self bounds].size.height;
752 // Colorspaces and CGContexts only happen with non-GL hacks.
754 CGColorSpaceRelease (colorspace);
755 # ifdef BACKBUFFER_CGCONTEXT
757 CGContextRelease (window_ctx);
760 NSWindow *window = [self window];
762 if (window && xdpy) {
765 # if defined(BACKBUFFER_CGCONTEXT)
766 // TODO: This was borrowed from jwxyz_window_resized, and should
767 // probably be refactored.
769 // Figure out which screen the window is currently on.
770 CGDirectDisplayID cgdpy = 0;
774 // TODO: XTranslateCoordinates is returning (0,1200) on my system.
776 // In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
777 // XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
782 p0 = [window convertBaseToScreen:p0];
783 CGPoint p = {p0.x, p0.y};
785 CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
786 NSAssert (cgdpy, @"unable to find CGDisplay");
790 // Figure out this screen's colorspace, and use that for every CGImage.
792 CMProfileRef profile = 0;
794 // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
795 // documented replacement as of OS X 10.9.
796 // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
797 CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
798 NSAssert (profile, @"unable to find colorspace profile");
799 colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
800 NSAssert (colorspace, @"unable to find colorspace");
802 # elif defined(BACKBUFFER_CALAYER)
803 // Was apparently faster until 10.9.
804 colorspace = CGColorSpaceCreateDeviceRGB ();
805 # endif // BACKBUFFER_CALAYER
807 # ifdef BACKBUFFER_CGCONTEXT
808 window_ctx = [[window graphicsContext] graphicsPort];
809 CGContextRetain (window_ctx);
810 # endif // BACKBUFFER_CGCONTEXT
814 # ifdef BACKBUFFER_CGCONTEXT
816 # endif // BACKBUFFER_CGCONTEXT
817 colorspace = CGColorSpaceCreateDeviceRGB();
821 backbuffer_size.width == new_w &&
822 backbuffer_size.height == new_h)
825 CGSize osize = backbuffer_size;
826 CGContextRef ob = backbuffer;
828 backbuffer_size.width = new_w;
829 backbuffer_size.height = new_h;
831 backbuffer = CGBitmapContextCreate (NULL,
832 backbuffer_size.width,
833 backbuffer_size.height,
835 backbuffer_size.width * 4,
837 // kCGImageAlphaPremultipliedLast
838 (kCGImageAlphaNoneSkipFirst |
839 kCGBitmapByteOrder32Host)
841 NSAssert (backbuffer, @"unable to allocate back buffer");
845 r.origin.x = r.origin.y = 0;
846 r.size = backbuffer_size;
847 CGContextSetGrayFillColor (backbuffer, 0, 1);
848 CGContextFillRect (backbuffer, r);
851 // Restore old bits, as much as possible, to the X11 upper left origin.
854 rect.origin.y = (backbuffer_size.height - osize.height);
856 CGImageRef img = CGBitmapContextCreateImage (ob);
857 CGContextDrawImage (backbuffer, rect, img);
858 CGImageRelease (img);
859 CGContextRelease (ob);
863 #endif // USE_BACKBUFFER
866 /* Inform X11 that the size of our window has changed.
870 if (!xwindow) return; // early
872 # ifdef USE_BACKBUFFER
873 [self createBackbuffer];
874 jwxyz_window_resized (xdpy, xwindow,
876 backbuffer_size.width, backbuffer_size.height,
878 # else // !USE_BACKBUFFER
879 NSRect r = [self frame]; // ignoring rotation is closer
880 r.size = [self bounds].size; // to what XGetGeometry expects.
881 jwxyz_window_resized (xdpy, xwindow,
882 r.origin.x, r.origin.y,
883 r.size.width, r.size.height,
885 # endif // !USE_BACKBUFFER
887 // Next time render_x11 is called, run the saver's reshape_cb.
897 if (orientation == UIDeviceOrientationUnknown)
898 [self didRotate:nil];
905 # ifdef USE_BACKBUFFER
906 NSAssert (backbuffer, @"no back buffer");
907 xdpy = jwxyz_make_display (self, backbuffer);
909 xdpy = jwxyz_make_display (self, 0);
911 xwindow = XRootWindow (xdpy, 0);
914 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
916 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
917 # endif // USE_IPHONE
925 xsft->setup_cb (xsft, xsft->setup_arg);
929 NSAssert(!xdata, @"xdata already initialized");
935 XSetWindowBackground (xdpy, xwindow,
936 get_pixel_resource (xdpy, 0,
937 "background", "Background"));
938 XClearWindow (xdpy, xwindow);
941 [[self window] setAcceptsMouseMovedEvents:YES];
944 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
945 drawing primitives will run on the GPU instead of the CPU.
946 It seems like it might make things worse rather than better,
947 though... Plus it makes us binary-incompatible with 10.4.
949 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
950 [[self window] setPreferredBackingLocation:
951 NSWindowBackingLocationVideoMemory];
955 /* Kludge: even though the init_cb functions are declared to take 2 args,
956 actually call them with 3, for the benefit of xlockmore_init() and
959 void *(*init_cb) (Display *, Window, void *) =
960 (void *(*) (Display *, Window, void *)) xsft->init_cb;
962 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
964 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
965 fpst = fps_init (xdpy, xwindow);
966 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
971 [self checkForUpdates];
975 /* I don't understand why we have to do this *every frame*, but we do,
976 or else the cursor comes back on.
979 if (![self isPreview])
980 [NSCursor setHiddenUntilMouseMoves:YES];
986 /* This is just a guess, but the -fps code wants to know how long
987 we were sleeping between frames.
989 long usecs = 1000000 * [self animationTimeInterval];
990 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
991 if (usecs < 0) usecs = 0;
992 fps_slept (fpst, usecs);
996 /* It turns out that on some systems (possibly only 10.5 and older?)
997 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
998 that we cannot rely on it.
1000 Some of the screen hacks want to delay for long periods, and letting the
1001 framework run the update function at 30 FPS when it really wanted half a
1002 minute between frames would be bad. So instead, we assume that the
1003 framework's animation timer might fire whenever, but we only invoke the
1004 screen hack's "draw frame" method when enough time has expired.
1006 This means two extra calls to gettimeofday() per frame. For fast-cycling
1007 screen savers, that might actually slow them down. Oh well.
1009 A side-effect of this is that it's not possible for a saver to request
1010 an animation interval that is faster than animationTimeInterval.
1012 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
1013 ignored, it's important that it be faster than 30 FPS. 120 FPS is good.
1015 An NSTimer won't fire if the timer is already running the invocation
1016 function from a previous firing. So, if we use a 30 FPS
1017 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
1018 frame, there will be a 26666 µs delay until the next frame, 66666 µs
1019 after the beginning of the current frame. In other words, 25 FPS
1022 Frame rates tend to snap to values of 30/N, where N is a positive
1023 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1024 is rounded down from what it would normally be.
1026 So if we set animationTimeInterval to 1/120 instead of 1/30, frame rates
1027 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1028 steps for higher or lower animation time intervals respectively.
1031 gettimeofday (&tv, 0);
1032 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1033 if (now < next_frame_time) return;
1035 [self prepareContext];
1038 // We do this here instead of in setFrame so that all the
1039 // Xlib drawing takes place under the animation timer.
1040 [self resizeContext];
1042 # ifndef USE_BACKBUFFER
1044 # else // USE_BACKBUFFER
1047 r.size.width = backbuffer_size.width;
1048 r.size.height = backbuffer_size.height;
1049 # endif // USE_BACKBUFFER
1051 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
1055 // Run any XtAppAddInput callbacks now.
1056 // (Note that XtAppAddTimeOut callbacks have already been run by
1057 // the Cocoa event loop.)
1059 jwxyz_sources_run (display_sources_data (xdpy));
1065 NSDisableScreenUpdates();
1067 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1068 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1070 NSEnableScreenUpdates();
1073 gettimeofday (&tv, 0);
1074 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1075 next_frame_time = now + (delay / 1000000.0);
1077 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1078 if (delay < [self animationTimeInterval])
1079 [self setAnimationTimeInterval:(delay / 1000000.0)];
1082 # ifdef DO_GC_HACKERY
1083 /* Current theory is that the 10.6 garbage collector sucks in the
1086 It only does a collection when a threshold of outstanding
1087 collectable allocations has been surpassed. However, CoreGraphics
1088 creates lots of small collectable allocations that contain pointers
1089 to very large non-collectable allocations: a small CG object that's
1090 collectable referencing large malloc'd allocations (non-collectable)
1091 containing bitmap data. So the large allocation doesn't get freed
1092 until GC collects the small allocation, which triggers its finalizer
1093 to run which frees the large allocation. So GC is deciding that it
1094 doesn't really need to run, even though the process has gotten
1095 enormous. GC eventually runs once pageouts have happened, but by
1096 then it's too late, and the machine's resident set has been
1099 So, we force an exhaustive garbage collection in this process
1100 approximately every 5 seconds whether the system thinks it needs
1104 static int tick = 0;
1105 if (++tick > 5*30) {
1107 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1110 # endif // DO_GC_HACKERY
1114 @catch (NSException *e) {
1115 [self handleException: e];
1117 # endif // USE_IPHONE
1121 /* drawRect always does nothing, and animateOneFrame renders bits to the
1122 screen. This is (now) true of both X11 and GL on both MacOS and iOS.
1125 - (void)drawRect:(NSRect)rect
1127 if (xwindow) // clear to the X window's bg color, not necessarily black.
1128 XClearWindow (xdpy, xwindow);
1130 [super drawRect:rect]; // early: black.
1134 #ifndef USE_BACKBUFFER
1136 - (void) animateOneFrame
1139 jwxyz_flush_context(xdpy);
1142 #else // USE_BACKBUFFER
1144 - (void) animateOneFrame
1146 // Render X11 into the backing store bitmap...
1148 NSAssert (backbuffer, @"no back buffer");
1151 UIGraphicsPushContext (backbuffer);
1157 UIGraphicsPopContext();
1161 // Then compute the transformations for rotation.
1162 double hs = [self hackedContentScaleFactor];
1163 double s = [self contentScaleFactor];
1165 // The rotation origin for layer.affineTransform is in the center already.
1166 CGAffineTransform t = ignore_rotation_p ?
1167 CGAffineTransformIdentity :
1168 CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1171 self.layer.affineTransform = CGAffineTransformScale(t, f, f);
1174 bounds.origin.x = 0;
1175 bounds.origin.y = 0;
1176 bounds.size.width = backbuffer_size.width / s;
1177 bounds.size.height = backbuffer_size.height / s;
1178 self.layer.bounds = bounds;
1179 # endif // USE_IPHONE
1181 # if defined(BACKBUFFER_CALAYER)
1182 [self.layer setNeedsDisplay];
1183 # elif defined(BACKBUFFER_CGCONTEXT)
1185 w = CGBitmapContextGetWidth (backbuffer),
1186 h = CGBitmapContextGetHeight (backbuffer);
1188 size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
1189 CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
1190 CGBitmapContextGetData(backbuffer),
1195 CGImageRef img = CGImageCreate (w, h,
1197 CGBitmapContextGetBytesPerRow(backbuffer),
1199 CGBitmapContextGetBitmapInfo(backbuffer),
1201 kCGRenderingIntentDefault);
1203 CGDataProviderRelease (prov);
1208 rect.size = backbuffer_size;
1209 CGContextDrawImage (window_ctx, rect, img);
1211 CGImageRelease (img);
1213 CGContextFlush (window_ctx);
1214 # endif // BACKBUFFER_CGCONTEXT
1217 # ifdef BACKBUFFER_CALAYER
1219 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1221 // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1222 char *dest_data = (char *)CGBitmapContextGetData (ctx);
1224 // The CGContext here is normally upside-down on iOS.
1226 CGBitmapContextGetBitmapInfo (ctx) ==
1227 (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1229 && CGContextGetCTM (ctx).d < 0
1230 # endif // USE_IPHONE
1233 size_t dest_height = CGBitmapContextGetHeight (ctx);
1234 size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1235 size_t src_height = CGBitmapContextGetHeight (backbuffer);
1236 size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1237 char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1239 size_t height = src_height < dest_height ? src_height : dest_height;
1241 if (src_bpr == dest_bpr) {
1242 // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1243 memcpy (dest_data, src_data, src_bpr * height);
1245 // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1246 size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1248 memcpy (dest_data, src_data, bpr);
1250 src_data += src_bpr;
1251 dest_data += dest_bpr;
1256 // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1259 CGContextScaleCTM (ctx, 1, -1);
1260 CGFloat s = [self contentScaleFactor];
1261 CGFloat hs = [self hackedContentScaleFactor];
1262 CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1263 # endif // USE_IPHONE
1265 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1266 CGContextDrawImage (ctx, self.layer.bounds, img);
1267 CGImageRelease (img);
1270 # endif // BACKBUFFER_CALAYER
1272 #endif // USE_BACKBUFFER
1276 - (void) setFrame:(NSRect) newRect
1278 [super setFrame:newRect];
1280 if (xwindow) // inform Xlib that the window has changed now.
1285 # ifndef USE_IPHONE // Doesn't exist on iOS
1286 - (void) setFrameSize:(NSSize) newSize
1288 [super setFrameSize:newSize];
1292 # endif // !USE_IPHONE
1295 +(BOOL) performGammaFade
1300 - (BOOL) hasConfigureSheet
1305 + (NSString *) decompressXML: (NSData *)data
1307 if (! data) return 0;
1308 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1310 // If it's not already XML, decompress it.
1311 NSAssert (compressed_p, @"xml isn't compressed");
1313 NSMutableData *data2 = 0;
1316 memset (&zs, 0, sizeof(zs));
1317 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1319 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1320 data2 = [NSMutableData dataWithLength: usize];
1321 zs.next_in = (Bytef *) data.bytes;
1322 zs.avail_in = (uint) data.length;
1323 zs.next_out = (Bytef *) data2.bytes;
1324 zs.avail_out = (uint) data2.length;
1325 ret = inflate (&zs, Z_FINISH);
1328 if (ret == Z_OK || ret == Z_STREAM_END)
1331 NSAssert2 (0, @"gunzip error: %d: %s",
1332 ret, (zs.msg ? zs.msg : "<null>"));
1335 return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1340 - (NSWindow *) configureSheet
1342 - (UIViewController *) configureView
1345 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1346 NSString *file = [NSString stringWithCString:xsft->progclass
1347 encoding:NSISOLatin1StringEncoding];
1348 file = [file lowercaseString];
1349 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1351 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1352 file, [bundle resourcePath]);
1357 UIViewController *sheet;
1358 # else // !USE_IPHONE
1360 # endif // !USE_IPHONE
1362 NSData *xmld = [NSData dataWithContentsOfFile:path];
1363 NSString *xml = [[self class] decompressXML: xmld];
1364 sheet = [[XScreenSaverConfigSheet alloc]
1365 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1366 options:xsft->options
1367 controller:[prefsReader userDefaultsController]
1368 globalController:[prefsReader globalDefaultsController]
1369 defaults:[prefsReader defaultOptions]];
1371 // #### am I expected to retain this, or not? wtf.
1372 // I thought not, but if I don't do this, we (sometimes) crash.
1373 // #### Analyze says "potential leak of an object stored into sheet"
1380 - (NSUserDefaultsController *) userDefaultsController
1382 return [prefsReader userDefaultsController];
1386 /* Announce our willingness to accept keyboard input.
1388 - (BOOL)acceptsFirstResponder
1396 /* Convert an NSEvent into an XEvent, and pass it along.
1397 Returns YES if it was handled.
1399 - (BOOL) doEvent: (NSEvent *) e
1402 if (![self isPreview] || // no event handling if actually screen-saving!
1403 ![self isAnimating] ||
1408 memset (&xe, 0, sizeof(xe));
1412 int flags = [e modifierFlags];
1413 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1414 if (flags & NSShiftKeyMask) state |= ShiftMask;
1415 if (flags & NSControlKeyMask) state |= ControlMask;
1416 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1417 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1419 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1422 double s = [self hackedContentScaleFactor];
1427 int y = s * ([self bounds].size.height - p.y);
1429 xe.xany.type = type;
1435 xe.xbutton.state = state;
1436 if ([e type] == NSScrollWheel)
1437 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1438 [e deltaY] < 0 ? Button5 :
1439 [e deltaX] > 0 ? Button6 :
1440 [e deltaX] < 0 ? Button7 :
1443 xe.xbutton.button = [e buttonNumber] + 1;
1448 xe.xmotion.state = state;
1453 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1454 [e charactersIgnoringModifiers]);
1457 if (!ns || [ns length] == 0) // dead key
1459 // Cocoa hides the difference between left and right keys.
1460 // Also we only get KeyPress events for these, no KeyRelease
1461 // (unless we hack the mod state manually. Bleh.)
1463 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1464 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1465 else if (flags & NSControlKeyMask) k = XK_Control_L;
1466 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1467 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1469 else if ([ns length] == 1) // real key
1471 switch ([ns characterAtIndex:0]) {
1472 case NSLeftArrowFunctionKey: k = XK_Left; break;
1473 case NSRightArrowFunctionKey: k = XK_Right; break;
1474 case NSUpArrowFunctionKey: k = XK_Up; break;
1475 case NSDownArrowFunctionKey: k = XK_Down; break;
1476 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1477 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1478 case NSHomeFunctionKey: k = XK_Home; break;
1479 case NSPrevFunctionKey: k = XK_Prior; break;
1480 case NSNextFunctionKey: k = XK_Next; break;
1481 case NSBeginFunctionKey: k = XK_Begin; break;
1482 case NSEndFunctionKey: k = XK_End; break;
1486 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1487 k = (s && *s ? *s : 0);
1493 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
1495 xe.xkey.keycode = k;
1496 xe.xkey.state = state;
1500 NSAssert1 (0, @"unknown X11 event type: %d", type);
1505 [self prepareContext];
1506 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1512 - (void) mouseDown: (NSEvent *) e
1514 if (! [self doEvent:e type:ButtonPress])
1515 [super mouseDown:e];
1518 - (void) mouseUp: (NSEvent *) e
1520 if (! [self doEvent:e type:ButtonRelease])
1524 - (void) otherMouseDown: (NSEvent *) e
1526 if (! [self doEvent:e type:ButtonPress])
1527 [super otherMouseDown:e];
1530 - (void) otherMouseUp: (NSEvent *) e
1532 if (! [self doEvent:e type:ButtonRelease])
1533 [super otherMouseUp:e];
1536 - (void) mouseMoved: (NSEvent *) e
1538 if (! [self doEvent:e type:MotionNotify])
1539 [super mouseMoved:e];
1542 - (void) mouseDragged: (NSEvent *) e
1544 if (! [self doEvent:e type:MotionNotify])
1545 [super mouseDragged:e];
1548 - (void) otherMouseDragged: (NSEvent *) e
1550 if (! [self doEvent:e type:MotionNotify])
1551 [super otherMouseDragged:e];
1554 - (void) scrollWheel: (NSEvent *) e
1556 if (! [self doEvent:e type:ButtonPress])
1557 [super scrollWheel:e];
1560 - (void) keyDown: (NSEvent *) e
1562 if (! [self doEvent:e type:KeyPress])
1566 - (void) keyUp: (NSEvent *) e
1568 if (! [self doEvent:e type:KeyRelease])
1572 - (void) flagsChanged: (NSEvent *) e
1574 if (! [self doEvent:e type:KeyPress])
1575 [super flagsChanged:e];
1581 - (void) stopAndClose:(Bool)relaunch_p
1583 if ([self isAnimating])
1584 [self stopAnimation];
1586 /* Need to make the SaverListController be the firstResponder again
1587 so that it can continue to receive its own shake events. I
1588 suppose that this abstraction-breakage means that I'm adding
1589 XScreenSaverView to the UINavigationController wrong...
1591 UIViewController *v = [[self window] rootViewController];
1592 if ([v isKindOfClass: [UINavigationController class]]) {
1593 UINavigationController *n = (UINavigationController *) v;
1594 [[n topViewController] becomeFirstResponder];
1597 UIView *fader = [self superview]; // the "backgroundView" view is our parent
1599 if (relaunch_p) { // Fake a shake on the SaverListController.
1600 // Why is [self window] sometimes null here?
1601 UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1602 UIViewController *v = [w rootViewController];
1603 if ([v isKindOfClass: [UINavigationController class]]) {
1604 UINavigationController *n = (UINavigationController *) v;
1605 [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1608 } else { // Not launching another, animate our return to the list.
1609 [UIView animateWithDuration: 0.5
1610 animations:^{ fader.alpha = 0.0; }
1611 completion:^(BOOL finished) {
1612 [fader removeFromSuperview];
1619 /* Called after the device's orientation has changed.
1621 Note: we could include a subclass of UIViewController which
1622 contains a shouldAutorotateToInterfaceOrientation method that
1623 returns YES, in which case Core Animation would auto-rotate our
1624 View for us in response to rotation events... but, that interacts
1625 badly with the EAGLContext -- if you introduce Core Animation into
1626 the path, the OpenGL pipeline probably falls back on software
1627 rendering and performance goes to hell. Also, the scaling and
1628 rotation that Core Animation does interacts incorrectly with the GL
1631 So, we have to hack the rotation animation manually, in the GL world.
1633 Possibly XScreenSaverView should use Core Animation, and
1634 XScreenSaverGLView should override that.
1636 - (void)didRotate:(NSNotification *)notification
1638 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1640 /* If the simulator starts up in the rotated position, sometimes
1641 the UIDevice says we're in Portrait when we're not -- but it
1642 turns out that the UINavigationController knows what's up!
1643 So get it from there.
1645 if (current == UIDeviceOrientationUnknown) {
1646 switch ([[[self window] rootViewController] interfaceOrientation]) {
1647 case UIInterfaceOrientationPortrait:
1648 current = UIDeviceOrientationPortrait;
1650 case UIInterfaceOrientationPortraitUpsideDown:
1651 current = UIDeviceOrientationPortraitUpsideDown;
1653 case UIInterfaceOrientationLandscapeLeft: // It's opposite day
1654 current = UIDeviceOrientationLandscapeRight;
1656 case UIInterfaceOrientationLandscapeRight:
1657 current = UIDeviceOrientationLandscapeLeft;
1664 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1665 an orientation change event with an unknown orientation. Those seem
1666 to always be immediately followed by another orientation change with
1667 a *real* orientation change, so let's try just ignoring those bogus
1668 ones and hoping that the real one comes in shortly...
1670 if (current == UIDeviceOrientationUnknown)
1673 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1674 if (orientation == current) return; // no change
1676 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1677 if (current == UIDeviceOrientationFaceUp ||
1678 current == UIDeviceOrientationFaceDown)
1681 new_orientation = current; // current animation target
1682 rotation_ratio = 0; // start animating
1683 rot_start_time = double_time();
1685 switch (orientation) {
1686 case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
1687 case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
1688 case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1689 default: angle_from = 0; break;
1692 switch (new_orientation) {
1693 case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
1694 case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
1695 case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1696 default: angle_to = 0; break;
1699 switch (orientation) {
1700 case UIDeviceOrientationLandscapeRight: // from landscape
1701 case UIDeviceOrientationLandscapeLeft:
1702 rot_from.width = initial_bounds.height;
1703 rot_from.height = initial_bounds.width;
1705 default: // from portrait
1706 rot_from.width = initial_bounds.width;
1707 rot_from.height = initial_bounds.height;
1711 switch (new_orientation) {
1712 case UIDeviceOrientationLandscapeRight: // to landscape
1713 case UIDeviceOrientationLandscapeLeft:
1714 rot_to.width = initial_bounds.height;
1715 rot_to.height = initial_bounds.width;
1717 default: // to portrait
1718 rot_to.width = initial_bounds.width;
1719 rot_to.height = initial_bounds.height;
1724 // If we've done a rotation but the saver hasn't been initialized yet,
1725 // don't bother going through an X11 resize, but just do it now.
1726 rot_start_time = 0; // dawn of time
1727 [self hackRotation];
1732 /* I believe we can't use UIGestureRecognizer for tracking touches
1733 because UIPanGestureRecognizer doesn't give us enough detail in its
1736 Currently we don't handle multi-touches (just the first touch) but
1737 I'm leaving this comment here for future reference:
1739 In the simulator, multi-touch sequences look like this:
1741 touchesBegan [touchA, touchB]
1742 touchesEnd [touchA, touchB]
1744 But on real devices, sometimes you get that, but sometimes you get:
1746 touchesBegan [touchA, touchB]
1752 touchesBegan [touchA]
1753 touchesBegan [touchB]
1757 So the only way to properly detect a "pinch" gesture is to remember
1758 the start-point of each touch as it comes in; and the end-point of
1759 each touch as those come in; and only process the gesture once the
1760 number of touchEnds matches the number of touchBegins.
1763 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1765 // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
1766 // Currently, this is the iPad Retina only.
1767 CGRect frame = [self bounds]; // Scale.
1768 double s = [self hackedContentScaleFactor];
1769 *x *= (backbuffer_size.width / frame.size.width) / s;
1770 *y *= (backbuffer_size.height / frame.size.height) / s;
1774 #if 0 // AudioToolbox/AudioToolbox.h
1777 // There's no way to play a standard system alert sound!
1778 // We'd have to include our own WAV for that. Eh, fuck it.
1779 AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1780 # if TARGET_IPHONE_SIMULATOR
1781 NSLog(@"BEEP"); // The sim doesn't vibrate.
1787 /* We distinguish between taps and drags.
1788 - Drags (down, motion, up) are sent to the saver to handle.
1789 - Single-taps exit the saver.
1790 This means a saver cannot respond to a single-tap. Only a few try to.
1793 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1795 // If they are trying to pinch, just do nothing.
1796 if ([[event allTouches] count] > 1)
1801 if (xsft->event_cb && xwindow) {
1802 double s = [self hackedContentScaleFactor];
1804 memset (&xe, 0, sizeof(xe));
1806 // #### 'frame' here or 'bounds'?
1807 int w = s * [self frame].size.width;
1808 int h = s * [self frame].size.height;
1809 for (UITouch *touch in touches) {
1810 CGPoint p = [touch locationInView:self];
1811 xe.xany.type = ButtonPress;
1812 xe.xbutton.button = i + 1;
1813 xe.xbutton.button = i + 1;
1814 xe.xbutton.x = s * p.x;
1815 xe.xbutton.y = s * p.y;
1816 [self rotateMouse: rot_current_angle
1817 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1818 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1820 // Ignore return code: don't care whether the hack handled it.
1821 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1823 // Remember when/where this was, to determine tap versus drag or hold.
1824 tap_time = double_time();
1828 break; // No pinches: only look at the first touch.
1834 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1836 // If they are trying to pinch, just do nothing.
1837 if ([[event allTouches] count] > 1)
1840 if (xsft->event_cb && xwindow) {
1841 double s = [self hackedContentScaleFactor];
1843 memset (&xe, 0, sizeof(xe));
1845 // #### 'frame' here or 'bounds'?
1846 int w = s * [self frame].size.width;
1847 int h = s * [self frame].size.height;
1848 for (UITouch *touch in touches) {
1849 CGPoint p = [touch locationInView:self];
1851 // If the ButtonRelease came less than half a second after ButtonPress,
1852 // and didn't move far, then this was a tap, not a drag or a hold.
1853 // Interpret it as "exit".
1855 double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1856 ((p.y - tap_point.y) * (p.y - tap_point.y)));
1857 if (tap_time + 0.5 >= double_time() && dist < 20) {
1858 [self stopAndClose:NO];
1862 xe.xany.type = ButtonRelease;
1863 xe.xbutton.button = i + 1;
1864 xe.xbutton.x = s * p.x;
1865 xe.xbutton.y = s * p.y;
1866 [self rotateMouse: rot_current_angle
1867 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1868 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1869 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1871 break; // No pinches: only look at the first touch.
1877 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1879 // If they are trying to pinch, just do nothing.
1880 if ([[event allTouches] count] > 1)
1883 if (xsft->event_cb && xwindow) {
1884 double s = [self hackedContentScaleFactor];
1886 memset (&xe, 0, sizeof(xe));
1888 // #### 'frame' here or 'bounds'?
1889 int w = s * [self frame].size.width;
1890 int h = s * [self frame].size.height;
1891 for (UITouch *touch in touches) {
1892 CGPoint p = [touch locationInView:self];
1893 xe.xany.type = MotionNotify;
1894 xe.xmotion.x = s * p.x;
1895 xe.xmotion.y = s * p.y;
1896 [self rotateMouse: rot_current_angle
1897 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1898 jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1899 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1901 break; // No pinches: only look at the first touch.
1907 /* We need this to respond to "shake" gestures
1909 - (BOOL)canBecomeFirstResponder
1914 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1919 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1923 /* Shake means exit and launch a new saver.
1925 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1927 [self stopAndClose:YES];
1931 - (void)setScreenLocked:(BOOL)locked
1933 if (screenLocked == locked) return;
1934 screenLocked = locked;
1936 if ([self isAnimating])
1937 [self stopAnimation];
1939 if (! [self isAnimating])
1940 [self startAnimation];
1944 #endif // USE_IPHONE
1947 - (void) checkForUpdates
1950 // We only check once at startup, even if there are multiple screens,
1951 // and even if this saver is running for many days.
1952 // (Uh, except this doesn't work because this static isn't shared,
1953 // even if we make it an exported global. Not sure why. Oh well.)
1954 static BOOL checked_p = NO;
1955 if (checked_p) return;
1958 // If it's off, don't bother running the updater. Otherwise, the
1959 // updater will decide if it's time to hit the network.
1960 if (! get_boolean_resource (xdpy,
1961 SUSUEnableAutomaticChecksKey,
1962 SUSUEnableAutomaticChecksKey))
1965 NSString *updater = @"XScreenSaverUpdater.app";
1967 // There may be multiple copies of the updater: e.g., one in /Applications
1968 // and one in the mounted installer DMG! It's important that we run the
1969 // one from the disk and not the DMG, so search for the right one.
1971 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
1972 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1974 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
1975 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
1976 @"/Library/Screen Savers",
1977 @"/System/Library/Screen Savers",
1979 @"/Applications/Utilities"];
1980 NSString *app_path = nil;
1981 for (NSString *dir in search) {
1982 NSString *p = [dir stringByAppendingPathComponent:updater];
1983 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
1990 app_path = [workspace fullPathForApplication:updater];
1992 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
1993 app_path = 0; // The DMG version will not do.
1996 NSLog(@"Unable to find %@", updater);
2001 if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2002 options:(NSWorkspaceLaunchWithoutAddingToRecents |
2003 NSWorkspaceLaunchWithoutActivation |
2004 NSWorkspaceLaunchAndHide)
2007 NSLog(@"Unable to launch %@: %@", app_path, err);
2010 # endif // !USE_IPHONE
2016 /* Utility functions...
2019 static PrefsReader *
2020 get_prefsReader (Display *dpy)
2022 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2023 if (!view) return 0;
2024 return [view prefsReader];
2029 get_string_resource (Display *dpy, char *name, char *class)
2031 return [get_prefsReader(dpy) getStringResource:name];
2035 get_boolean_resource (Display *dpy, char *name, char *class)
2037 return [get_prefsReader(dpy) getBooleanResource:name];
2041 get_integer_resource (Display *dpy, char *name, char *class)
2043 return [get_prefsReader(dpy) getIntegerResource:name];
2047 get_float_resource (Display *dpy, char *name, char *class)
2049 return [get_prefsReader(dpy) getFloatResource:name];