/* xscreensaver, Copyright (c) 2006-2010 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 "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; @implementation XScreenSaverView - (struct xscreensaver_function_table *) findFunctionTable { NSBundle *nsb = [NSBundle bundleForClass:[self class]]; NSAssert1 (nsb, @"no bundle for class %@", [self class]); NSString *path = [nsb bundlePath]; NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension] lowercaseString]; NSString *suffix = @"_xscreensaver_function_table"; NSString *table_name = [name stringByAppendingString:suffix]; CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef) path, kCFURLPOSIXPathStyle, true); CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url); CFRelease (url); NSAssert1 (cfb, @"no CFBundle for \"%@\"", path); void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name); NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path); // NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr); 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"); abort(); } /* 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"); abort(); } /* 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 }, { "-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"}, { 0, 0, 0, 0 } }; static const char *default_defaults [] = { ".doFPS: False", ".doubleBuffer: True", // for most OpenGL hacks ".textMode: date", // ".textLiteral: ", // ".textFile: ", // ".textURL: ", ".grabDesktopImages: yes", ".chooseRandomImages: no", ".imageDirectory: ~/Pictures", 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; } - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview { if (! (self = [super initWithFrame:frame isPreview:isPreview])) return 0; xsft = [self findFunctionTable]; [self setShellPath]; 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:NSUTF8StringEncoding]; 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; return self; } - (void) dealloc { NSAssert(![self isAnimating], @"still animating"); NSAssert(!xdata, @"xdata not yet freed"); if (xdpy) jwxyz_free_display (xdpy); [prefsReader release]; [super dealloc]; } - (PrefsReader *) prefsReader { return prefsReader; } - (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. */ } - (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; } [super stopAnimation]; } /* 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); fps_draw (fpst); } - (void) animateOneFrame { if (!initted_p) { if (! xdpy) { xdpy = jwxyz_make_display (self); xwindow = XRootWindow (xdpy, 0); } 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); [[self window] setAcceptsMouseMovedEvents:YES]; /* 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. */ if (![self isPreview]) [NSCursor setHiddenUntilMouseMoves:YES]; if (fpst) { /* This is just a guess, but the -fps code wants to know how long we were sleeping between frames. */ unsigned long usecs = 1000000 * [self animationTimeInterval]; usecs -= 200; // caller apparently sleeps for slightly less sometimes... 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 setFrameSize so that all the // Xlib drawing takes place under the animation timer. [self resizeContext]; NSRect r = [self frame]; 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: // NSDisableScreenUpdates(); unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata); if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata); XSync (xdpy, 0); NSEnableScreenUpdates(); gettimeofday (&tv, 0); now = tv.tv_sec + (tv.tv_usec / 1000000.0); next_frame_time = now + (delay / 1000000.0); # 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 } - (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. } - (void) setFrameSize:(NSSize) newSize { [super setFrameSize:newSize]; if ([self isAnimating]) { resized_p = YES; } } - (void) setFrame:(NSRect) newRect { [super setFrame:newRect]; if (xwindow) // inform Xlib that the window has changed. jwxyz_window_resized (xdpy, xwindow); } +(BOOL) performGammaFade { return YES; } - (BOOL) hasConfigureSheet { return YES; } - (NSWindow *) configureSheet { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *file = [NSString stringWithCString:xsft->progclass encoding:NSUTF8StringEncoding]; 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; } NSWindow *sheet = [[XScreenSaverConfigSheet alloc] initWithXMLFile:path options:xsft->options controller:[prefsReader userDefaultsController]]; // #### am I expected to retain this, or not? wtf. // I thought not, but if I don't do this, we (sometimes) crash. [sheet retain]; return sheet; } /* Announce our willingness to accept keyboard input. */ - (BOOL)acceptsFirstResponder { return YES; } /* 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]; int x = p.x; int y = [self frame].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 *nss = [e characters]; const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding]; xe.xkey.keycode = (s && *s ? *s : 0); xe.xkey.state = state; break; } default: abort(); } [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]; } @end /* Utility functions... */ static PrefsReader * get_prefsReader (Display *dpy) { XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0)); if (!view) abort(); 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]; }