/* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation. No representations are made about the suitability of this * software for any purpose. It is provided "as is" without express or * implied warranty. */ /* This is a subclass of Apple's ScreenSaverView that knows how to run xscreensaver programs without X11 via the dark magic of the "jwxyz" library. In xscreensaver terminology, this is the replacement for the "screenhack.c" module. */ #import #import #import "XScreenSaverView.h" #import "XScreenSaverConfigSheet.h" #import "screenhackI.h" #import "xlockmoreI.h" #import "jwxyz-timers.h" /* Garbage collection only exists if we are being compiled against the 10.6 SDK or newer, not if we are building against the 10.4 SDK. */ #ifndef MAC_OS_X_VERSION_10_6 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */ #endif #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */ # import # define DO_GC_HACKERY #endif extern struct xscreensaver_function_table *xscreensaver_function_table; /* Global variables used by the screen savers */ const char *progname; const char *progclass; int mono_p = 0; # ifdef USE_IPHONE extern NSDictionary *make_function_table_dict(void); // ios-function-table.m /* Stub definition of the superclass, for iPhone. */ @implementation ScreenSaverView { NSTimeInterval anim_interval; Bool animating_p; NSTimer *anim_timer; } - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview { self = [super initWithFrame:frame]; if (! self) return 0; anim_interval = 1.0/30; return self; } - (NSTimeInterval)animationTimeInterval { return anim_interval; } - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; } - (BOOL)hasConfigureSheet { return NO; } - (NSWindow *)configureSheet { return nil; } - (NSView *)configureView { return nil; } - (BOOL)isPreview { return NO; } - (BOOL)isAnimating { return animating_p; } - (void)animateOneFrame { } - (void)startAnimation { if (animating_p) return; animating_p = YES; anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval target:self selector:@selector(animateOneFrame) userInfo:nil repeats:YES]; } - (void)stopAnimation { if (anim_timer) { [anim_timer invalidate]; anim_timer = 0; } animating_p = NO; } @end # endif // !USE_IPHONE @interface XScreenSaverView (Private) - (void) stopAndClose:(Bool)relaunch; @end @implementation XScreenSaverView // Given a lower-cased saver name, returns the function table for it. // If no name, guess the name from the class's bundle name. // - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name { NSBundle *nsb = [NSBundle bundleForClass:[self class]]; NSAssert1 (nsb, @"no bundle for class %@", [self class]); NSString *path = [nsb bundlePath]; CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef) path, kCFURLPOSIXPathStyle, true); CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url); CFRelease (url); NSAssert1 (cfb, @"no CFBundle for \"%@\"", path); // #### Analyze says "Potential leak of an object stored into cfb" if (! name) name = [[path lastPathComponent] stringByDeletingPathExtension]; name = [[name lowercaseString] stringByReplacingOccurrencesOfString:@" " withString:@""]; # ifndef USE_IPHONE // CFBundleGetDataPointerForName doesn't work in "Archive" builds. // I'm guessing that symbol-stripping is mandatory. Fuck. NSString *table_name = [name stringByAppendingString: @"_xscreensaver_function_table"]; void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name); CFRelease (cfb); if (! addr) NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path); # else // USE_IPHONE // Remember: any time you add a new saver to the iOS app, // manually run "make ios-function-table.m"! if (! function_tables) function_tables = [make_function_table_dict() retain]; NSValue *v = [function_tables objectForKey: name]; void *addr = v ? [v pointerValue] : 0; # endif // USE_IPHONE return (struct xscreensaver_function_table *) addr; } // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle // to $PATH for the benefit of savers that include helper shell scripts. // - (void) setShellPath { NSBundle *nsb = [NSBundle bundleForClass:[self class]]; NSAssert1 (nsb, @"no bundle for class %@", [self class]); NSString *nsdir = [nsb resourcePath]; NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]); const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding]; const char *opath = getenv ("PATH"); if (!opath) opath = "/bin"; // $PATH is unset when running under Shark! char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30); strcpy (npath, "PATH="); strcat (npath, dir); strcat (npath, ":"); strcat (npath, opath); if (putenv (npath)) { perror ("putenv"); NSAssert1 (0, @"putenv \"%s\" failed", npath); } /* Don't free (npath) -- MacOS's putenv() does not copy it. */ } // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts // (e.g., "xscreensaver-text") know how to look up resources. // - (void) setResourcesEnv:(NSString *) name { NSBundle *nsb = [NSBundle bundleForClass:[self class]]; NSAssert1 (nsb, @"no bundle for class %@", [self class]); const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding]; char *env = (char *) malloc (strlen (s) + 40); strcpy (env, "XSCREENSAVER_CLASSPATH="); strcat (env, s); if (putenv (env)) { perror ("putenv"); NSAssert1 (0, @"putenv \"%s\" failed", env); } /* Don't free (env) -- MacOS's putenv() does not copy it. */ } static void add_default_options (const XrmOptionDescRec *opts, const char * const *defs, XrmOptionDescRec **opts_ret, const char ***defs_ret) { /* These aren't "real" command-line options (there are no actual command-line options in the Cocoa version); but this is the somewhat kludgey way that the and tags in the ../hacks/config/\*.xml files communicate with the preferences database. */ static const XrmOptionDescRec default_options [] = { { "-text-mode", ".textMode", XrmoptionSepArg, 0 }, { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 }, { "-text-file", ".textFile", XrmoptionSepArg, 0 }, { "-text-url", ".textURL", XrmoptionSepArg, 0 }, { "-text-program", ".textProgram", XrmoptionSepArg, 0 }, { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" }, { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"}, { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" }, { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"}, { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 }, { "-fps", ".doFPS", XrmoptionNoArg, "True" }, { "-no-fps", ".doFPS", XrmoptionNoArg, "False"}, { "-foreground", ".foreground", XrmoptionSepArg, 0 }, { "-fg", ".foreground", XrmoptionSepArg, 0 }, { "-background", ".background", XrmoptionSepArg, 0 }, { "-bg", ".background", XrmoptionSepArg, 0 }, { 0, 0, 0, 0 } }; static const char *default_defaults [] = { ".doFPS: False", ".doubleBuffer: True", ".multiSample: False", # ifndef USE_IPHONE ".textMode: date", # else ".textMode: url", # endif // ".textLiteral: ", // ".textFile: ", ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss", // ".textProgram: ", ".grabDesktopImages: yes", # ifndef USE_IPHONE ".chooseRandomImages: no", # else ".chooseRandomImages: yes", # endif ".imageDirectory: ~/Pictures", ".relaunchDelay: 2", 0 }; int count = 0, i, j; for (i = 0; default_options[i].option; i++) count++; for (i = 0; opts[i].option; i++) count++; XrmOptionDescRec *opts2 = (XrmOptionDescRec *) calloc (count + 1, sizeof (*opts2)); i = 0; j = 0; while (default_options[j].option) { opts2[i] = default_options[j]; i++, j++; } j = 0; while (opts[j].option) { opts2[i] = opts[j]; i++, j++; } *opts_ret = opts2; /* now the defaults */ count = 0; for (i = 0; default_defaults[i]; i++) count++; for (i = 0; defs[i]; i++) count++; const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2)); i = 0; j = 0; while (default_defaults[j]) { defs2[i] = default_defaults[j]; i++, j++; } j = 0; while (defs[j]) { defs2[i] = defs[j]; i++, j++; } *defs_ret = defs2; } #ifdef USE_IPHONE /* Returns the current time in seconds as a double. */ static double double_time (void) { struct timeval now; # ifdef GETTIMEOFDAY_TWO_ARGS struct timezone tzp; gettimeofday(&now, &tzp); # else gettimeofday(&now); # endif return (now.tv_sec + ((double) now.tv_usec * 0.000001)); } #endif // USE_IPHONE - (id) initWithFrame:(NSRect)frame saverName:(NSString *)saverName isPreview:(BOOL)isPreview { # ifdef USE_IPHONE initial_bounds = frame.size; rot_current_size = frame.size; // needs to be early, because rot_from = rot_current_size; // [self setFrame] is called by rot_to = rot_current_size; // [super initWithFrame]. rotation_ratio = -1; # endif if (! (self = [super initWithFrame:frame isPreview:isPreview])) return 0; xsft = [self findFunctionTable: saverName]; if (! xsft) { [self release]; return 0; } [self setShellPath]; # ifdef USE_IPHONE [self setMultipleTouchEnabled:YES]; orientation = UIDeviceOrientationUnknown; [self didRotate:nil]; # endif // USE_IPHONE setup_p = YES; if (xsft->setup_cb) xsft->setup_cb (xsft, xsft->setup_arg); /* The plist files for these preferences show up in $HOME/Library/Preferences/ByHost/ in a file named like "org.jwz.xscreensaver...plist" */ NSString *name = [NSString stringWithCString:xsft->progclass encoding:NSISOLatin1StringEncoding]; name = [@"org.jwz.xscreensaver." stringByAppendingString:name]; [self setResourcesEnv:name]; XrmOptionDescRec *opts = 0; const char **defs = 0; add_default_options (xsft->options, xsft->defaults, &opts, &defs); prefsReader = [[PrefsReader alloc] initWithName:name xrmKeys:opts defaults:defs]; free (defs); // free (opts); // bah, we need these! #### leak! xsft->options = opts; progname = progclass = xsft->progclass; next_frame_time = 0; # ifdef USE_BACKBUFFER [self createBackbuffer]; [self initLayer]; # endif # ifdef USE_IPHONE // So we can tell when we're docked. [UIDevice currentDevice].batteryMonitoringEnabled = YES; # endif // USE_IPHONE return self; } - (void) initLayer { # ifndef USE_IPHONE [self setLayer: [CALayer layer]]; self.layer.delegate = self; self.layer.opaque = YES; [self setWantsLayer: YES]; # endif } - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p { return [self initWithFrame:frame saverName:0 isPreview:p]; } - (void) dealloc { NSAssert(![self isAnimating], @"still animating"); NSAssert(!xdata, @"xdata not yet freed"); if (xdpy) jwxyz_free_display (xdpy); # ifdef USE_BACKBUFFER if (backbuffer) CGContextRelease (backbuffer); # endif [prefsReader release]; // xsft // fpst [super dealloc]; } - (PrefsReader *) prefsReader { return prefsReader; } #ifdef USE_IPHONE - (void) lockFocus { } - (void) unlockFocus { } #endif // USE_IPHONE # ifdef USE_IPHONE /* A few seconds after the saver launches, we store the "wasRunning" preference. This is so that if the saver is crashing at startup, we don't launch it again next time, getting stuck in a crash loop. */ - (void) allSystemsGo: (NSTimer *) timer { NSAssert (timer == crash_timer, @"crash timer screwed up"); crash_timer = 0; NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; [prefs setBool:YES forKey:@"wasRunning"]; [prefs synchronize]; } #endif // USE_IPHONE - (void) startAnimation { NSAssert(![self isAnimating], @"already animating"); NSAssert(!initted_p && !xdata, @"already initialized"); [super startAnimation]; /* We can't draw on the window from this method, so we actually do the initialization of the screen saver (xsft->init_cb) in the first call to animateOneFrame() instead. */ # ifdef USE_IPHONE if (crash_timer) [crash_timer invalidate]; NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; [prefs removeObjectForKey:@"wasRunning"]; [prefs synchronize]; crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5 target:self selector:@selector(allSystemsGo:) userInfo:nil repeats:NO]; # endif // USE_IPHONE // Never automatically turn the screen off if we are docked, // and an animation is running. // # ifdef USE_IPHONE [UIApplication sharedApplication].idleTimerDisabled = ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged); # endif } - (void)stopAnimation { NSAssert([self isAnimating], @"not animating"); if (initted_p) { [self lockFocus]; // in case something tries to draw from here [self prepareContext]; /* I considered just not even calling the free callback at all... But webcollage-cocoa needs it, to kill the inferior webcollage processes (since the screen saver framework never generates a SIGPIPE for them...) Instead, I turned off the free call in xlockmore.c, which is where all of the bogus calls are anyway. */ xsft->free_cb (xdpy, xwindow, xdata); [self unlockFocus]; // setup_p = NO; // #### wait, do we need this? initted_p = NO; xdata = 0; } # ifdef USE_IPHONE if (crash_timer) [crash_timer invalidate]; crash_timer = 0; NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; [prefs removeObjectForKey:@"wasRunning"]; [prefs synchronize]; # endif // USE_IPHONE [super stopAnimation]; // When an animation is no longer running (e.g., looking at the list) // then it's ok to power off the screen when docked. // # ifdef USE_IPHONE [UIApplication sharedApplication].idleTimerDisabled = NO; # endif } /* Hook for the XScreenSaverGLView subclass */ - (void) prepareContext { } /* Hook for the XScreenSaverGLView subclass */ - (void) resizeContext { } static void screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) { fps_compute (fpst, 0, -1); fps_draw (fpst); } #ifdef USE_IPHONE /* On iPhones with Retina displays, we can draw the savers in "real" pixels, and that works great. The 320x480 "point" screen is really a 640x960 *pixel* screen. However, Retina iPads have 768x1024 point screens which are 1536x2048 pixels, and apparently that's enough pixels that copying those bits to the screen is slow. Like, drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in real pixels. This will probably make the savers look better anyway, since that's a higher resolution than most desktop monitors have even today. (This is only true for X11 programs, not GL programs. Those are fine at full rez.) This method is overridden in XScreenSaverGLView, since this kludge isn't necessary for GL programs, being resolution independent by nature. */ - (CGFloat) hackedContentScaleFactor { GLfloat s = [self contentScaleFactor]; if (initial_bounds.width >= 1024 || initial_bounds.height >= 1024) s = 1; return s; } static GLfloat _global_rot_current_angle_kludge; double current_device_rotation (void) { return -_global_rot_current_angle_kludge; } - (void) hackRotation { if (rotation_ratio >= 0) { // in the midst of a rotation animation # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360 GLfloat f = angle_from; GLfloat t = angle_to; CLAMP180(f); CLAMP180(t); GLfloat dist = -(t-f); CLAMP180(dist); // Intermediate angle. rot_current_angle = f - rotation_ratio * dist; // Intermediate frame size. rot_current_size.width = rot_from.width + rotation_ratio * (rot_to.width - rot_from.width); rot_current_size.height = rot_from.height + rotation_ratio * (rot_to.height - rot_from.height); // Tick animation. Complete rotation in 1/6th sec. double now = double_time(); double duration = 1/6.0; rotation_ratio = 1 - ((rot_start_time + duration - now) / duration); if (rotation_ratio > 1) { // Done animating. orientation = new_orientation; rot_current_angle = angle_to; rot_current_size = rot_to; rotation_ratio = -1; // Check orientation again in case we rotated again while rotating: // this is a no-op if nothing has changed. [self didRotate:nil]; } } else { // Not animating a rotation. rot_current_angle = angle_to; rot_current_size = rot_to; } CLAMP180(rot_current_angle); _global_rot_current_angle_kludge = rot_current_angle; # undef CLAMP180 double s = [self hackedContentScaleFactor]; if (!ignore_rotation_p && /* rotation_ratio && */ ((int) backbuffer_size.width != (int) (s * rot_current_size.width) || (int) backbuffer_size.height != (int) (s * rot_current_size.height))) [self resize_x11]; } - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i { if (i == 0) exit (-1); // Cancel [self stopAndClose:NO]; // Keep going } - (void) handleException: (NSException *)e { NSLog (@"Caught exception: %@", e); [[[UIAlertView alloc] initWithTitle: [NSString stringWithFormat: @"%s crashed!", xsft->progclass] message: [NSString stringWithFormat: @"The error message was:" "\n\n%@\n\n" "If it keeps crashing, try " "resetting its options.", e] delegate: self cancelButtonTitle: @"Exit" otherButtonTitles: @"Keep going", nil] show]; [self stopAnimation]; } #endif // USE_IPHONE #ifdef USE_BACKBUFFER /* Create a bitmap context into which we render everything. If the desired size has changed, re-created it. */ - (void) createBackbuffer { # ifdef USE_IPHONE double s = [self hackedContentScaleFactor]; CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size; int new_w = s * rotsize.width; int new_h = s * rotsize.height; # else int new_w = [self bounds].size.width; int new_h = [self bounds].size.height; # endif if (backbuffer && backbuffer_size.width == new_w && backbuffer_size.height == new_h) return; CGSize osize = backbuffer_size; CGContextRef ob = backbuffer; backbuffer_size.width = new_w; backbuffer_size.height = new_h; CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); backbuffer = CGBitmapContextCreate (NULL, backbuffer_size.width, backbuffer_size.height, 8, backbuffer_size.width * 4, cs, // kCGImageAlphaPremultipliedLast (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host) ); CGColorSpaceRelease (cs); NSAssert (backbuffer, @"unable to allocate back buffer"); // Clear it. CGRect r; r.origin.x = r.origin.y = 0; r.size = backbuffer_size; CGContextSetGrayFillColor (backbuffer, 0, 1); CGContextFillRect (backbuffer, r); if (ob) { // Restore old bits, as much as possible, to the X11 upper left origin. CGRect rect; rect.origin.x = 0; rect.origin.y = (backbuffer_size.height - osize.height); rect.size = osize; CGImageRef img = CGBitmapContextCreateImage (ob); CGContextDrawImage (backbuffer, rect, img); CGImageRelease (img); CGContextRelease (ob); } } #endif // USE_BACKBUFFER /* Inform X11 that the size of our window has changed. */ - (void) resize_x11 { if (!xwindow) return; // early # ifdef USE_BACKBUFFER [self createBackbuffer]; jwxyz_window_resized (xdpy, xwindow, 0, 0, backbuffer_size.width, backbuffer_size.height, backbuffer); # else // !USE_BACKBUFFER NSRect r = [self frame]; // ignoring rotation is closer r.size = [self bounds].size; // to what XGetGeometry expects. jwxyz_window_resized (xdpy, xwindow, r.origin.x, r.origin.y, r.size.width, r.size.height, 0); # endif // !USE_BACKBUFFER // Next time render_x11 is called, run the saver's reshape_cb. resized_p = YES; } - (void) render_x11 { # ifdef USE_IPHONE @try { if (orientation == UIDeviceOrientationUnknown) [self didRotate:nil]; [self hackRotation]; # endif if (!initted_p) { if (! xdpy) { # ifdef USE_BACKBUFFER NSAssert (backbuffer, @"no back buffer"); xdpy = jwxyz_make_display (self, backbuffer); # else xdpy = jwxyz_make_display (self, 0); # endif xwindow = XRootWindow (xdpy, 0); # ifdef USE_IPHONE /* Some X11 hacks (fluidballs) want to ignore all rotation events. */ ignore_rotation_p = get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation"); # endif // USE_IPHONE [self resize_x11]; } if (!setup_p) { setup_p = YES; if (xsft->setup_cb) xsft->setup_cb (xsft, xsft->setup_arg); } initted_p = YES; resized_p = NO; NSAssert(!xdata, @"xdata already initialized"); # undef ya_rand_init ya_rand_init (0); XSetWindowBackground (xdpy, xwindow, get_pixel_resource (xdpy, 0, "background", "Background")); XClearWindow (xdpy, xwindow); # ifndef USE_IPHONE [[self window] setAcceptsMouseMovedEvents:YES]; # endif /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz drawing primitives will run on the GPU instead of the CPU. It seems like it might make things worse rather than better, though... Plus it makes us binary-incompatible with 10.4. # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 [[self window] setPreferredBackingLocation: NSWindowBackingLocationVideoMemory]; # endif */ /* Kludge: even though the init_cb functions are declared to take 2 args, actually call them with 3, for the benefit of xlockmore_init() and xlockmore_setup(). */ void *(*init_cb) (Display *, Window, void *) = (void *(*) (Display *, Window, void *)) xsft->init_cb; xdata = init_cb (xdpy, xwindow, xsft->setup_arg); if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) { fpst = fps_init (xdpy, xwindow); if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps; } } /* I don't understand why we have to do this *every frame*, but we do, or else the cursor comes back on. */ # ifndef USE_IPHONE if (![self isPreview]) [NSCursor setHiddenUntilMouseMoves:YES]; # endif if (fpst) { /* This is just a guess, but the -fps code wants to know how long we were sleeping between frames. */ long usecs = 1000000 * [self animationTimeInterval]; usecs -= 200; // caller apparently sleeps for slightly less sometimes... if (usecs < 0) usecs = 0; fps_slept (fpst, usecs); } /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing. This is bad, because some of the screen hacks want to delay for long periods (like 5 seconds or a minute!) between frames, and running them all at 60 FPS is no good. So, we don't use setAnimationTimeInterval, and just let the framework call us whenever. But, we only invoke the screen hack's "draw frame" method when enough time has expired. This means two extra calls to gettimeofday() per frame. For fast-cycling screen savers, that might actually slow them down. Oh well. #### Also, we do not run the draw callback faster than the system's animationTimeInterval, so if any savers are pickier about timing than that, this may slow them down too much. If that's a problem, then we could call draw_cb in a loop here (with usleep) until the next call would put us past animationTimeInterval... But a better approach would probably be to just change the saver to not do that. */ struct timeval tv; gettimeofday (&tv, 0); double now = tv.tv_sec + (tv.tv_usec / 1000000.0); if (now < next_frame_time) return; [self prepareContext]; if (resized_p) { // We do this here instead of in setFrame so that all the // Xlib drawing takes place under the animation timer. [self resizeContext]; NSRect r; # ifndef USE_BACKBUFFER r = [self bounds]; # else // USE_BACKBUFFER r.origin.x = 0; r.origin.y = 0; r.size.width = backbuffer_size.width; r.size.height = backbuffer_size.height; # endif // USE_BACKBUFFER xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height); resized_p = NO; } // Run any XtAppAddInput callbacks now. // (Note that XtAppAddTimeOut callbacks have already been run by // the Cocoa event loop.) // jwxyz_sources_run (display_sources_data (xdpy)); // And finally: // # ifndef USE_IPHONE NSDisableScreenUpdates(); # endif unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata); if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata); # ifndef USE_IPHONE NSEnableScreenUpdates(); # endif gettimeofday (&tv, 0); now = tv.tv_sec + (tv.tv_usec / 1000000.0); next_frame_time = now + (delay / 1000000.0); # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt. if (delay < [self animationTimeInterval]) [self setAnimationTimeInterval:(delay / 1000000.0)]; # endif # ifdef DO_GC_HACKERY /* Current theory is that the 10.6 garbage collector sucks in the following way: It only does a collection when a threshold of outstanding collectable allocations has been surpassed. However, CoreGraphics creates lots of small collectable allocations that contain pointers to very large non-collectable allocations: a small CG object that's collectable referencing large malloc'd allocations (non-collectable) containing bitmap data. So the large allocation doesn't get freed until GC collects the small allocation, which triggers its finalizer to run which frees the large allocation. So GC is deciding that it doesn't really need to run, even though the process has gotten enormous. GC eventually runs once pageouts have happened, but by then it's too late, and the machine's resident set has been sodomized. So, we force an exhaustive garbage collection in this process approximately every 5 seconds whether the system thinks it needs one or not. */ { static int tick = 0; if (++tick > 5*30) { tick = 0; objc_collect (OBJC_EXHAUSTIVE_COLLECTION); } } # endif // DO_GC_HACKERY # ifdef USE_IPHONE } @catch (NSException *e) { [self handleException: e]; } # endif // USE_IPHONE } /* drawRect always does nothing, and animateOneFrame renders bits to the screen. This is (now) true of both X11 and GL on both MacOS and iOS. */ - (void)drawRect:(NSRect)rect { if (xwindow) // clear to the X window's bg color, not necessarily black. XClearWindow (xdpy, xwindow); else [super drawRect:rect]; // early: black. } #ifndef USE_BACKBUFFER - (void) animateOneFrame { [self render_x11]; } #else // USE_BACKBUFFER - (void) animateOneFrame { // Render X11 into the backing store bitmap... NSAssert (backbuffer, @"no back buffer"); # ifdef USE_IPHONE UIGraphicsPushContext (backbuffer); # endif [self render_x11]; # ifdef USE_IPHONE UIGraphicsPopContext(); # endif # ifdef USE_IPHONE // Then compute the transformations for rotation. double hs = [self hackedContentScaleFactor]; double s = [self contentScaleFactor]; // The rotation origin for layer.affineTransform is in the center already. CGAffineTransform t = ignore_rotation_p ? CGAffineTransformIdentity : CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI)); CGFloat f = s / hs; self.layer.affineTransform = CGAffineTransformScale(t, f, f); CGRect bounds; bounds.origin.x = 0; bounds.origin.y = 0; bounds.size.width = backbuffer_size.width / s; bounds.size.height = backbuffer_size.height / s; self.layer.bounds = bounds; # endif // USE_IPHONE [self.layer setNeedsDisplay]; } - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4. char *dest_data = (char *)CGBitmapContextGetData (ctx); // The CGContext here is normally upside-down on iOS. if (dest_data && CGBitmapContextGetBitmapInfo (ctx) == (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host) #ifdef USE_IPHONE && CGContextGetCTM (ctx).d < 0 #endif ) { size_t dest_height = CGBitmapContextGetHeight (ctx); size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx); size_t src_height = CGBitmapContextGetHeight (backbuffer); size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer); char *src_data = (char *)CGBitmapContextGetData (backbuffer); size_t height = src_height < dest_height ? src_height : dest_height; if (src_bpr == dest_bpr) { // iPad 1: 4.0 ms, iPad 2: 6.7 ms memcpy (dest_data, src_data, src_bpr * height); } else { // iPad 1: 4.6 ms, iPad 2: 7.2 ms size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr; while (height) { memcpy (dest_data, src_data, bpr); --height; src_data += src_bpr; dest_data += dest_bpr; } } } else { // iPad 1: 9.6 ms, iPad 2: 12.1 ms #ifdef USE_IPHONE CGContextScaleCTM (ctx, 1, -1); CGFloat s = [self contentScaleFactor]; CGFloat hs = [self hackedContentScaleFactor]; CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s); #endif CGImageRef img = CGBitmapContextCreateImage (backbuffer); CGContextDrawImage (ctx, self.layer.bounds, img); CGImageRelease (img); } } #endif // !USE_BACKBUFFER - (void) setFrame:(NSRect) newRect { [super setFrame:newRect]; if (xwindow) // inform Xlib that the window has changed now. [self resize_x11]; } # ifndef USE_IPHONE // Doesn't exist on iOS - (void) setFrameSize:(NSSize) newSize { [super setFrameSize:newSize]; if (xwindow) [self resize_x11]; } # endif // !USE_IPHONE +(BOOL) performGammaFade { return YES; } - (BOOL) hasConfigureSheet { return YES; } + (NSString *) decompressXML: (NSData *)data { if (! data) return 0; BOOL compressed_p = !!strncmp ((const char *) data.bytes, "")); } return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } #ifndef USE_IPHONE - (NSWindow *) configureSheet #else - (UIViewController *) configureView #endif { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *file = [NSString stringWithCString:xsft->progclass encoding:NSISOLatin1StringEncoding]; file = [file lowercaseString]; NSString *path = [bundle pathForResource:file ofType:@"xml"]; if (!path) { NSLog (@"%@.xml does not exist in the application bundle: %@/", file, [bundle resourcePath]); return nil; } # ifdef USE_IPHONE UIViewController *sheet; # else // !USE_IPHONE NSWindow *sheet; # endif // !USE_IPHONE NSData *xmld = [NSData dataWithContentsOfFile:path]; NSString *xml = [[self class] decompressXML: xmld]; sheet = [[XScreenSaverConfigSheet alloc] initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding] options:xsft->options controller:[prefsReader userDefaultsController] defaults:[prefsReader defaultOptions]]; // #### am I expected to retain this, or not? wtf. // I thought not, but if I don't do this, we (sometimes) crash. // #### Analyze says "potential leak of an object stored into sheet" [sheet retain]; return sheet; } - (NSUserDefaultsController *) userDefaultsController { return [prefsReader userDefaultsController]; } /* Announce our willingness to accept keyboard input. */ - (BOOL)acceptsFirstResponder { return YES; } #ifndef USE_IPHONE /* Convert an NSEvent into an XEvent, and pass it along. Returns YES if it was handled. */ - (BOOL) doEvent: (NSEvent *) e type: (int) type { if (![self isPreview] || // no event handling if actually screen-saving! ![self isAnimating] || !initted_p) return NO; XEvent xe; memset (&xe, 0, sizeof(xe)); int state = 0; int flags = [e modifierFlags]; if (flags & NSAlphaShiftKeyMask) state |= LockMask; if (flags & NSShiftKeyMask) state |= ShiftMask; if (flags & NSControlKeyMask) state |= ControlMask; if (flags & NSAlternateKeyMask) state |= Mod1Mask; if (flags & NSCommandKeyMask) state |= Mod2Mask; NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow] toView:self]; # ifdef USE_IPHONE double s = [self hackedContentScaleFactor]; # else int s = 1; # endif int x = s * p.x; int y = s * ([self bounds].size.height - p.y); xe.xany.type = type; switch (type) { case ButtonPress: case ButtonRelease: xe.xbutton.x = x; xe.xbutton.y = y; xe.xbutton.state = state; if ([e type] == NSScrollWheel) xe.xbutton.button = ([e deltaY] > 0 ? Button4 : [e deltaY] < 0 ? Button5 : [e deltaX] > 0 ? Button6 : [e deltaX] < 0 ? Button7 : 0); else xe.xbutton.button = [e buttonNumber] + 1; break; case MotionNotify: xe.xmotion.x = x; xe.xmotion.y = y; xe.xmotion.state = state; break; case KeyPress: case KeyRelease: { NSString *ns = (([e type] == NSFlagsChanged) ? 0 : [e charactersIgnoringModifiers]); KeySym k = 0; if (!ns || [ns length] == 0) // dead key { // Cocoa hides the difference between left and right keys. // Also we only get KeyPress events for these, no KeyRelease // (unless we hack the mod state manually. Bleh.) // if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock; else if (flags & NSShiftKeyMask) k = XK_Shift_L; else if (flags & NSControlKeyMask) k = XK_Control_L; else if (flags & NSAlternateKeyMask) k = XK_Alt_L; else if (flags & NSCommandKeyMask) k = XK_Meta_L; } else if ([ns length] == 1) // real key { switch ([ns characterAtIndex:0]) { case NSLeftArrowFunctionKey: k = XK_Left; break; case NSRightArrowFunctionKey: k = XK_Right; break; case NSUpArrowFunctionKey: k = XK_Up; break; case NSDownArrowFunctionKey: k = XK_Down; break; case NSPageUpFunctionKey: k = XK_Page_Up; break; case NSPageDownFunctionKey: k = XK_Page_Down; break; case NSHomeFunctionKey: k = XK_Home; break; case NSPrevFunctionKey: k = XK_Prior; break; case NSNextFunctionKey: k = XK_Next; break; case NSBeginFunctionKey: k = XK_Begin; break; case NSEndFunctionKey: k = XK_End; break; default: { const char *s = [ns cStringUsingEncoding:NSISOLatin1StringEncoding]; k = (s && *s ? *s : 0); } break; } } if (! k) return YES; // E.g., "KeyRelease XK_Shift_L" xe.xkey.keycode = k; xe.xkey.state = state; break; } default: NSAssert1 (0, @"unknown X11 event type: %d", type); break; } [self lockFocus]; [self prepareContext]; BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe); [self unlockFocus]; return result; } - (void) mouseDown: (NSEvent *) e { if (! [self doEvent:e type:ButtonPress]) [super mouseDown:e]; } - (void) mouseUp: (NSEvent *) e { if (! [self doEvent:e type:ButtonRelease]) [super mouseUp:e]; } - (void) otherMouseDown: (NSEvent *) e { if (! [self doEvent:e type:ButtonPress]) [super otherMouseDown:e]; } - (void) otherMouseUp: (NSEvent *) e { if (! [self doEvent:e type:ButtonRelease]) [super otherMouseUp:e]; } - (void) mouseMoved: (NSEvent *) e { if (! [self doEvent:e type:MotionNotify]) [super mouseMoved:e]; } - (void) mouseDragged: (NSEvent *) e { if (! [self doEvent:e type:MotionNotify]) [super mouseDragged:e]; } - (void) otherMouseDragged: (NSEvent *) e { if (! [self doEvent:e type:MotionNotify]) [super otherMouseDragged:e]; } - (void) scrollWheel: (NSEvent *) e { if (! [self doEvent:e type:ButtonPress]) [super scrollWheel:e]; } - (void) keyDown: (NSEvent *) e { if (! [self doEvent:e type:KeyPress]) [super keyDown:e]; } - (void) keyUp: (NSEvent *) e { if (! [self doEvent:e type:KeyRelease]) [super keyUp:e]; } - (void) flagsChanged: (NSEvent *) e { if (! [self doEvent:e type:KeyPress]) [super flagsChanged:e]; } #else // USE_IPHONE - (void) stopAndClose:(Bool)relaunch_p { if ([self isAnimating]) [self stopAnimation]; /* Need to make the SaverListController be the firstResponder again so that it can continue to receive its own shake events. I suppose that this abstraction-breakage means that I'm adding XScreenSaverView to the UINavigationController wrong... */ UIViewController *v = [[self window] rootViewController]; if ([v isKindOfClass: [UINavigationController class]]) { UINavigationController *n = (UINavigationController *) v; [[n topViewController] becomeFirstResponder]; } UIView *fader = [self superview]; // the "backgroundView" view is our parent if (relaunch_p) { // Fake a shake on the SaverListController. // Why is [self window] sometimes null here? UIWindow *w = [[UIApplication sharedApplication] keyWindow]; UIViewController *v = [w rootViewController]; if ([v isKindOfClass: [UINavigationController class]]) { UINavigationController *n = (UINavigationController *) v; [[n topViewController] motionEnded: UIEventSubtypeMotionShake withEvent: nil]; } } else { // Not launching another, animate our return to the list. [UIView animateWithDuration: 0.5 animations:^{ fader.alpha = 0.0; } completion:^(BOOL finished) { [fader removeFromSuperview]; fader.alpha = 1.0; }]; } } /* Called after the device's orientation has changed. Note: we could include a subclass of UIViewController which contains a shouldAutorotateToInterfaceOrientation method that returns YES, in which case Core Animation would auto-rotate our View for us in response to rotation events... but, that interacts badly with the EAGLContext -- if you introduce Core Animation into the path, the OpenGL pipeline probably falls back on software rendering and performance goes to hell. Also, the scaling and rotation that Core Animation does interacts incorrectly with the GL context anyway. So, we have to hack the rotation animation manually, in the GL world. Possibly XScreenSaverView should use Core Animation, and XScreenSaverGLView should override that. */ - (void)didRotate:(NSNotification *)notification { UIDeviceOrientation current = [[UIDevice currentDevice] orientation]; /* If the simulator starts up in the rotated position, sometimes the UIDevice says we're in Portrait when we're not -- but it turns out that the UINavigationController knows what's up! So get it from there. */ if (current == UIDeviceOrientationUnknown) { switch ([[[self window] rootViewController] interfaceOrientation]) { case UIInterfaceOrientationPortrait: current = UIDeviceOrientationPortrait; break; case UIInterfaceOrientationPortraitUpsideDown: current = UIDeviceOrientationPortraitUpsideDown; break; case UIInterfaceOrientationLandscapeLeft: // It's opposite day current = UIDeviceOrientationLandscapeRight; break; case UIInterfaceOrientationLandscapeRight: current = UIDeviceOrientationLandscapeLeft; break; default: break; } } /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get an orientation change event with an unknown orientation. Those seem to always be immediately followed by another orientation change with a *real* orientation change, so let's try just ignoring those bogus ones and hoping that the real one comes in shortly... */ if (current == UIDeviceOrientationUnknown) return; if (rotation_ratio >= 0) return; // in the midst of rotation animation if (orientation == current) return; // no change // When transitioning to FaceUp or FaceDown, pretend there was no change. if (current == UIDeviceOrientationFaceUp || current == UIDeviceOrientationFaceDown) return; new_orientation = current; // current animation target rotation_ratio = 0; // start animating rot_start_time = double_time(); switch (orientation) { case UIDeviceOrientationLandscapeLeft: angle_from = 90; break; case UIDeviceOrientationLandscapeRight: angle_from = 270; break; case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break; default: angle_from = 0; break; } switch (new_orientation) { case UIDeviceOrientationLandscapeLeft: angle_to = 90; break; case UIDeviceOrientationLandscapeRight: angle_to = 270; break; case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break; default: angle_to = 0; break; } switch (orientation) { case UIDeviceOrientationLandscapeRight: // from landscape case UIDeviceOrientationLandscapeLeft: rot_from.width = initial_bounds.height; rot_from.height = initial_bounds.width; break; default: // from portrait rot_from.width = initial_bounds.width; rot_from.height = initial_bounds.height; break; } switch (new_orientation) { case UIDeviceOrientationLandscapeRight: // to landscape case UIDeviceOrientationLandscapeLeft: rot_to.width = initial_bounds.height; rot_to.height = initial_bounds.width; break; default: // to portrait rot_to.width = initial_bounds.width; rot_to.height = initial_bounds.height; break; } if (! initted_p) { // If we've done a rotation but the saver hasn't been initialized yet, // don't bother going through an X11 resize, but just do it now. rot_start_time = 0; // dawn of time [self hackRotation]; } } /* I believe we can't use UIGestureRecognizer for tracking touches because UIPanGestureRecognizer doesn't give us enough detail in its callbacks. Currently we don't handle multi-touches (just the first touch) but I'm leaving this comment here for future reference: In the simulator, multi-touch sequences look like this: touchesBegan [touchA, touchB] touchesEnd [touchA, touchB] But on real devices, sometimes you get that, but sometimes you get: touchesBegan [touchA, touchB] touchesEnd [touchB] touchesEnd [touchA] Or even touchesBegan [touchA] touchesBegan [touchB] touchesEnd [touchA] touchesEnd [touchB] So the only way to properly detect a "pinch" gesture is to remember the start-point of each touch as it comes in; and the end-point of each touch as those come in; and only process the gesture once the number of touchEnds matches the number of touchBegins. */ - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h { // This is a no-op unless contentScaleFactor != hackedContentScaleFactor. // Currently, this is the iPad Retina only. CGRect frame = [self bounds]; // Scale. double s = [self hackedContentScaleFactor]; *x *= (backbuffer_size.width / frame.size.width) / s; *y *= (backbuffer_size.height / frame.size.height) / s; } #if 0 // AudioToolbox/AudioToolbox.h - (void) beep { // There's no way to play a standard system alert sound! // We'd have to include our own WAV for that. Eh, fuck it. AudioServicesPlaySystemSound (kSystemSoundID_Vibrate); # if TARGET_IPHONE_SIMULATOR NSLog(@"BEEP"); // The sim doesn't vibrate. # endif } #endif /* We distinguish between taps and drags. - Drags (down, motion, up) are sent to the saver to handle. - Single-taps exit the saver. This means a saver cannot respond to a single-tap. Only a few try to. */ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // If they are trying to pinch, just do nothing. if ([[event allTouches] count] > 1) return; tap_time = 0; if (xsft->event_cb && xwindow) { double s = [self hackedContentScaleFactor]; XEvent xe; memset (&xe, 0, sizeof(xe)); int i = 0; // #### 'frame' here or 'bounds'? int w = s * [self frame].size.width; int h = s * [self frame].size.height; for (UITouch *touch in touches) { CGPoint p = [touch locationInView:self]; xe.xany.type = ButtonPress; xe.xbutton.button = i + 1; xe.xbutton.button = i + 1; xe.xbutton.x = s * p.x; xe.xbutton.y = s * p.y; [self rotateMouse: rot_current_angle x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); // Ignore return code: don't care whether the hack handled it. xsft->event_cb (xdpy, xwindow, xdata, &xe); // Remember when/where this was, to determine tap versus drag or hold. tap_time = double_time(); tap_point = p; i++; break; // No pinches: only look at the first touch. } } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // If they are trying to pinch, just do nothing. if ([[event allTouches] count] > 1) return; if (xsft->event_cb && xwindow) { double s = [self hackedContentScaleFactor]; XEvent xe; memset (&xe, 0, sizeof(xe)); int i = 0; // #### 'frame' here or 'bounds'? int w = s * [self frame].size.width; int h = s * [self frame].size.height; for (UITouch *touch in touches) { CGPoint p = [touch locationInView:self]; // If the ButtonRelease came less than half a second after ButtonPress, // and didn't move far, then this was a tap, not a drag or a hold. // Interpret it as "exit". // double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) + ((p.y - tap_point.y) * (p.y - tap_point.y))); if (tap_time + 0.5 >= double_time() && dist < 20) { [self stopAndClose:NO]; return; } xe.xany.type = ButtonRelease; xe.xbutton.button = i + 1; xe.xbutton.x = s * p.x; xe.xbutton.y = s * p.y; [self rotateMouse: rot_current_angle x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); xsft->event_cb (xdpy, xwindow, xdata, &xe); i++; break; // No pinches: only look at the first touch. } } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { // If they are trying to pinch, just do nothing. if ([[event allTouches] count] > 1) return; if (xsft->event_cb && xwindow) { double s = [self hackedContentScaleFactor]; XEvent xe; memset (&xe, 0, sizeof(xe)); int i = 0; // #### 'frame' here or 'bounds'? int w = s * [self frame].size.width; int h = s * [self frame].size.height; for (UITouch *touch in touches) { CGPoint p = [touch locationInView:self]; xe.xany.type = MotionNotify; xe.xmotion.x = s * p.x; xe.xmotion.y = s * p.y; [self rotateMouse: rot_current_angle x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y); xsft->event_cb (xdpy, xwindow, xdata, &xe); i++; break; // No pinches: only look at the first touch. } } } /* We need this to respond to "shake" gestures */ - (BOOL)canBecomeFirstResponder { return YES; } - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { } - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event { } /* Shake means exit and launch a new saver. */ - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { [self stopAndClose:YES]; } - (void)setScreenLocked:(BOOL)locked { if (screenLocked == locked) return; screenLocked = locked; if (locked) { if ([self isAnimating]) [self stopAnimation]; } else { if (! [self isAnimating]) [self startAnimation]; } } #endif // USE_IPHONE @end /* Utility functions... */ static PrefsReader * get_prefsReader (Display *dpy) { XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0)); if (!view) return 0; return [view prefsReader]; } char * get_string_resource (Display *dpy, char *name, char *class) { return [get_prefsReader(dpy) getStringResource:name]; } Bool get_boolean_resource (Display *dpy, char *name, char *class) { return [get_prefsReader(dpy) getBooleanResource:name]; } int get_integer_resource (Display *dpy, char *name, char *class) { return [get_prefsReader(dpy) getIntegerResource:name]; } double get_float_resource (Display *dpy, char *name, char *class) { return [get_prefsReader(dpy) getFloatResource:name]; }