1 /* xscreensaver, Copyright (c) 2006-2010 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 "XScreenSaverView.h"
19 #import "XScreenSaverConfigSheet.h"
20 #import "screenhackI.h"
21 #import "xlockmoreI.h"
22 #import "jwxyz-timers.h"
24 /* Garbage collection only exists if we are being compiled against the
25 10.6 SDK or newer, not if we are building against the 10.4 SDK.
27 #ifndef MAC_OS_X_VERSION_10_6
28 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
30 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */
31 # import <objc/objc-auto.h>
32 # define DO_GC_HACKERY
35 extern struct xscreensaver_function_table *xscreensaver_function_table;
37 /* Global variables used by the screen savers
40 const char *progclass;
44 @implementation XScreenSaverView
46 - (struct xscreensaver_function_table *) findFunctionTable
48 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
49 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
51 NSString *path = [nsb bundlePath];
52 NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension]
54 NSString *suffix = @"_xscreensaver_function_table";
55 NSString *table_name = [name stringByAppendingString:suffix];
57 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
61 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
63 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
65 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
66 NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path);
68 // NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr);
69 return (struct xscreensaver_function_table *) addr;
73 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
74 // to $PATH for the benefit of savers that include helper shell scripts.
78 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
79 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
81 NSString *nsdir = [nsb resourcePath];
82 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
83 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
84 const char *opath = getenv ("PATH");
85 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
86 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
87 strcpy (npath, "PATH=");
90 strcat (npath, opath);
96 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
100 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
101 // (e.g., "xscreensaver-text") know how to look up resources.
103 - (void) setResourcesEnv:(NSString *) name
105 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
106 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
108 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
109 char *env = (char *) malloc (strlen (s) + 40);
110 strcpy (env, "XSCREENSAVER_CLASSPATH=");
116 /* Don't free (env) -- MacOS's putenv() does not copy it. */
121 add_default_options (const XrmOptionDescRec *opts,
122 const char * const *defs,
123 XrmOptionDescRec **opts_ret,
124 const char ***defs_ret)
126 /* These aren't "real" command-line options (there are no actual command-line
127 options in the Cocoa version); but this is the somewhat kludgey way that
128 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
129 ../hacks/config/*.xml files communicate with the preferences database.
131 static const XrmOptionDescRec default_options [] = {
132 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
133 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
134 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
135 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
136 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
137 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
138 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
139 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
140 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
141 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
142 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
145 static const char *default_defaults [] = {
147 ".doubleBuffer: True", // for most OpenGL hacks
152 ".grabDesktopImages: yes",
153 ".chooseRandomImages: no",
154 ".imageDirectory: ~/Pictures",
159 for (i = 0; default_options[i].option; i++)
161 for (i = 0; opts[i].option; i++)
164 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
165 calloc (count + 1, sizeof (*opts2));
169 while (default_options[j].option) {
170 opts2[i] = default_options[j];
174 while (opts[j].option) {
185 for (i = 0; default_defaults[i]; i++)
187 for (i = 0; defs[i]; i++)
190 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
194 while (default_defaults[j]) {
195 defs2[i] = default_defaults[j];
208 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
210 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
213 xsft = [self findFunctionTable];
218 xsft->setup_cb (xsft, xsft->setup_arg);
220 /* The plist files for these preferences show up in
221 $HOME/Library/Preferences/ByHost/ in a file named like
222 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
224 NSString *name = [NSString stringWithCString:xsft->progclass
225 encoding:NSUTF8StringEncoding];
226 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
227 [self setResourcesEnv:name];
230 XrmOptionDescRec *opts = 0;
231 const char **defs = 0;
232 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
233 prefsReader = [[PrefsReader alloc]
234 initWithName:name xrmKeys:opts defaults:defs];
236 // free (opts); // bah, we need these! #### leak!
237 xsft->options = opts;
239 progname = progclass = xsft->progclass;
248 NSAssert(![self isAnimating], @"still animating");
249 NSAssert(!xdata, @"xdata not yet freed");
251 jwxyz_free_display (xdpy);
252 [prefsReader release];
256 - (PrefsReader *) prefsReader
262 - (void) startAnimation
264 NSAssert(![self isAnimating], @"already animating");
265 NSAssert(!initted_p && !xdata, @"already initialized");
266 [super startAnimation];
267 /* We can't draw on the window from this method, so we actually do the
268 initialization of the screen saver (xsft->init_cb) in the first call
269 to animateOneFrame() instead.
273 - (void)stopAnimation
275 NSAssert([self isAnimating], @"not animating");
279 [self lockFocus]; // in case something tries to draw from here
280 [self prepareContext];
282 /* I considered just not even calling the free callback at all...
283 But webcollage-cocoa needs it, to kill the inferior webcollage
284 processes (since the screen saver framework never generates a
285 SIGPIPE for them...) Instead, I turned off the free call in
286 xlockmore.c, which is where all of the bogus calls are anyway.
288 xsft->free_cb (xdpy, xwindow, xdata);
291 // setup_p = NO; // #### wait, do we need this?
296 [super stopAnimation];
300 /* Hook for the XScreenSaverGLView subclass
302 - (void) prepareContext
306 /* Hook for the XScreenSaverGLView subclass
308 - (void) resizeContext
314 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
316 fps_compute (fpst, 0);
321 - (void) animateOneFrame
326 xdpy = jwxyz_make_display (self);
327 xwindow = XRootWindow (xdpy, 0);
333 xsft->setup_cb (xsft, xsft->setup_arg);
337 NSAssert(!xdata, @"xdata already initialized");
342 XSetWindowBackground (xdpy, xwindow,
343 get_pixel_resource (xdpy, 0,
344 "background", "Background"));
345 XClearWindow (xdpy, xwindow);
347 [[self window] setAcceptsMouseMovedEvents:YES];
349 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
350 drawing primitives will run on the GPU instead of the CPU.
351 It seems like it might make things worse rather than better,
352 though... Plus it makes us binary-incompatible with 10.4.
354 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
355 [[self window] setPreferredBackingLocation:
356 NSWindowBackingLocationVideoMemory];
360 /* Kludge: even though the init_cb functions are declared to take 2 args,
361 actually call them with 3, for the benefit of xlockmore_init() and
364 void *(*init_cb) (Display *, Window, void *) =
365 (void *(*) (Display *, Window, void *)) xsft->init_cb;
367 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
369 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
370 fpst = fps_init (xdpy, xwindow);
371 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
375 /* I don't understand why we have to do this *every frame*, but we do,
376 or else the cursor comes back on.
378 if (![self isPreview])
379 [NSCursor setHiddenUntilMouseMoves:YES];
384 /* This is just a guess, but the -fps code wants to know how long
385 we were sleeping between frames.
387 unsigned long usecs = 1000000 * [self animationTimeInterval];
388 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
389 fps_slept (fpst, usecs);
393 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
394 This is bad, because some of the screen hacks want to delay for long
395 periods (like 5 seconds or a minute!) between frames, and running them
396 all at 60 FPS is no good.
398 So, we don't use setAnimationTimeInterval, and just let the framework call
399 us whenever. But, we only invoke the screen hack's "draw frame" method
400 when enough time has expired.
402 This means two extra calls to gettimeofday() per frame. For fast-cycling
403 screen savers, that might actually slow them down. Oh well.
405 #### Also, we do not run the draw callback faster than the system's
406 animationTimeInterval, so if any savers are pickier about timing
407 than that, this may slow them down too much. If that's a problem,
408 then we could call draw_cb in a loop here (with usleep) until the
409 next call would put us past animationTimeInterval... But a better
410 approach would probably be to just change the saver to not do that.
413 gettimeofday (&tv, 0);
414 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
415 if (now < next_frame_time) return;
417 [self prepareContext];
420 // We do this here instead of in setFrameSize so that all the
421 // Xlib drawing takes place under the animation timer.
422 [self resizeContext];
423 NSRect r = [self frame];
424 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
428 // Run any XtAppAddInput callbacks now.
429 // (Note that XtAppAddTimeOut callbacks have already been run by
430 // the Cocoa event loop.)
432 jwxyz_sources_run (display_sources_data (xdpy));
436 NSDisableScreenUpdates();
437 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
438 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
440 NSEnableScreenUpdates();
442 gettimeofday (&tv, 0);
443 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
444 next_frame_time = now + (delay / 1000000.0);
447 # ifdef DO_GC_HACKERY
448 /* Current theory is that the 10.6 garbage collector sucks in the
451 It only does a collection when a threshold of outstanding
452 collectable allocations has been surpassed. However, CoreGraphics
453 creates lots of small collectable allocations that contain pointers
454 to very large non-collectable allocations: a small CG object that's
455 collectable referencing large malloc'd allocations (non-collectable)
456 containing bitmap data. So the large allocation doesn't get freed
457 until GC collects the small allocation, which triggers its finalizer
458 to run which frees the large allocation. So GC is deciding that it
459 doesn't really need to run, even though the process has gotten
460 enormous. GC eventually runs once pageouts have happened, but by
461 then it's too late, and the machine's resident set has been
464 So, we force an exhaustive garbage collection in this process
465 approximately every 5 seconds whether the system thinks it needs
472 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
475 # endif // DO_GC_HACKERY
479 - (void)drawRect:(NSRect)rect
481 if (xwindow) // clear to the X window's bg color, not necessarily black.
482 XClearWindow (xdpy, xwindow);
484 [super drawRect:rect]; // early: black.
488 - (void) setFrameSize:(NSSize) newSize
490 [super setFrameSize:newSize];
491 if ([self isAnimating]) {
496 - (void) setFrame:(NSRect) newRect
498 [super setFrame:newRect];
499 if (xwindow) // inform Xlib that the window has changed.
500 jwxyz_window_resized (xdpy, xwindow);
504 +(BOOL) performGammaFade
509 - (BOOL) hasConfigureSheet
514 - (NSWindow *) configureSheet
516 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
517 NSString *file = [NSString stringWithCString:xsft->progclass
518 encoding:NSUTF8StringEncoding];
519 file = [file lowercaseString];
520 NSString *path = [bundle pathForResource:file ofType:@"xml"];
522 NSLog (@"%@.xml does not exist in the application bundle: %@/",
523 file, [bundle resourcePath]);
527 NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
529 options:xsft->options
530 controller:[prefsReader userDefaultsController]];
532 // #### am I expected to retain this, or not? wtf.
533 // I thought not, but if I don't do this, we (sometimes) crash.
540 /* Announce our willingness to accept keyboard input.
542 - (BOOL)acceptsFirstResponder
548 /* Convert an NSEvent into an XEvent, and pass it along.
549 Returns YES if it was handled.
551 - (BOOL) doEvent: (NSEvent *) e
554 if (![self isPreview] || // no event handling if actually screen-saving!
555 ![self isAnimating] ||
560 memset (&xe, 0, sizeof(xe));
564 int flags = [e modifierFlags];
565 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
566 if (flags & NSShiftKeyMask) state |= ShiftMask;
567 if (flags & NSControlKeyMask) state |= ControlMask;
568 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
569 if (flags & NSCommandKeyMask) state |= Mod2Mask;
571 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
574 int y = [self frame].size.height - p.y;
582 xe.xbutton.state = state;
583 if ([e type] == NSScrollWheel)
584 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
585 [e deltaY] < 0 ? Button5 :
586 [e deltaX] > 0 ? Button6 :
587 [e deltaX] < 0 ? Button7 :
590 xe.xbutton.button = [e buttonNumber] + 1;
595 xe.xmotion.state = state;
600 NSString *nss = [e characters];
601 const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding];
602 xe.xkey.keycode = (s && *s ? *s : 0);
603 xe.xkey.state = state;
611 [self prepareContext];
612 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
618 - (void) mouseDown: (NSEvent *) e
620 if (! [self doEvent:e type:ButtonPress])
624 - (void) mouseUp: (NSEvent *) e
626 if (! [self doEvent:e type:ButtonRelease])
630 - (void) otherMouseDown: (NSEvent *) e
632 if (! [self doEvent:e type:ButtonPress])
633 [super otherMouseDown:e];
636 - (void) otherMouseUp: (NSEvent *) e
638 if (! [self doEvent:e type:ButtonRelease])
639 [super otherMouseUp:e];
642 - (void) mouseMoved: (NSEvent *) e
644 if (! [self doEvent:e type:MotionNotify])
645 [super mouseMoved:e];
648 - (void) mouseDragged: (NSEvent *) e
650 if (! [self doEvent:e type:MotionNotify])
651 [super mouseDragged:e];
654 - (void) otherMouseDragged: (NSEvent *) e
656 if (! [self doEvent:e type:MotionNotify])
657 [super otherMouseDragged:e];
660 - (void) scrollWheel: (NSEvent *) e
662 if (! [self doEvent:e type:ButtonPress])
663 [super scrollWheel:e];
666 - (void) keyDown: (NSEvent *) e
668 if (! [self doEvent:e type:KeyPress])
672 - (void) keyUp: (NSEvent *) e
674 if (! [self doEvent:e type:KeyRelease])
681 /* Utility functions...
685 get_prefsReader (Display *dpy)
687 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
689 return [view prefsReader];
694 get_string_resource (Display *dpy, char *name, char *class)
696 return [get_prefsReader(dpy) getStringResource:name];
700 get_boolean_resource (Display *dpy, char *name, char *class)
702 return [get_prefsReader(dpy) getBooleanResource:name];
706 get_integer_resource (Display *dpy, char *name, char *class)
708 return [get_prefsReader(dpy) getIntegerResource:name];
712 get_float_resource (Display *dpy, char *name, char *class)
714 return [get_prefsReader(dpy) getFloatResource:name];