1 /* xscreensaver, Copyright (c) 2006-2011 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);
69 // NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr);
70 return (struct xscreensaver_function_table *) addr;
74 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
75 // to $PATH for the benefit of savers that include helper shell scripts.
79 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
80 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
82 NSString *nsdir = [nsb resourcePath];
83 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
84 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
85 const char *opath = getenv ("PATH");
86 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
87 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
88 strcpy (npath, "PATH=");
91 strcat (npath, opath);
97 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
101 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
102 // (e.g., "xscreensaver-text") know how to look up resources.
104 - (void) setResourcesEnv:(NSString *) name
106 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
107 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
109 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
110 char *env = (char *) malloc (strlen (s) + 40);
111 strcpy (env, "XSCREENSAVER_CLASSPATH=");
117 /* Don't free (env) -- MacOS's putenv() does not copy it. */
122 add_default_options (const XrmOptionDescRec *opts,
123 const char * const *defs,
124 XrmOptionDescRec **opts_ret,
125 const char ***defs_ret)
127 /* These aren't "real" command-line options (there are no actual command-line
128 options in the Cocoa version); but this is the somewhat kludgey way that
129 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
130 ../hacks/config/*.xml files communicate with the preferences database.
132 static const XrmOptionDescRec default_options [] = {
133 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
134 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
135 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
136 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
137 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
138 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
139 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
140 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
141 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
142 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
143 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
146 static const char *default_defaults [] = {
148 ".doubleBuffer: True",
149 ".multiSample: False",
154 ".grabDesktopImages: yes",
155 ".chooseRandomImages: no",
156 ".imageDirectory: ~/Pictures",
161 for (i = 0; default_options[i].option; i++)
163 for (i = 0; opts[i].option; i++)
166 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
167 calloc (count + 1, sizeof (*opts2));
171 while (default_options[j].option) {
172 opts2[i] = default_options[j];
176 while (opts[j].option) {
187 for (i = 0; default_defaults[i]; i++)
189 for (i = 0; defs[i]; i++)
192 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
196 while (default_defaults[j]) {
197 defs2[i] = default_defaults[j];
210 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
212 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
215 xsft = [self findFunctionTable];
220 xsft->setup_cb (xsft, xsft->setup_arg);
222 /* The plist files for these preferences show up in
223 $HOME/Library/Preferences/ByHost/ in a file named like
224 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
226 NSString *name = [NSString stringWithCString:xsft->progclass
227 encoding:NSUTF8StringEncoding];
228 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
229 [self setResourcesEnv:name];
232 XrmOptionDescRec *opts = 0;
233 const char **defs = 0;
234 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
235 prefsReader = [[PrefsReader alloc]
236 initWithName:name xrmKeys:opts defaults:defs];
238 // free (opts); // bah, we need these! #### leak!
239 xsft->options = opts;
241 progname = progclass = xsft->progclass;
250 NSAssert(![self isAnimating], @"still animating");
251 NSAssert(!xdata, @"xdata not yet freed");
253 jwxyz_free_display (xdpy);
254 [prefsReader release];
258 - (PrefsReader *) prefsReader
264 - (void) startAnimation
266 NSAssert(![self isAnimating], @"already animating");
267 NSAssert(!initted_p && !xdata, @"already initialized");
268 [super startAnimation];
269 /* We can't draw on the window from this method, so we actually do the
270 initialization of the screen saver (xsft->init_cb) in the first call
271 to animateOneFrame() instead.
275 - (void)stopAnimation
277 NSAssert([self isAnimating], @"not animating");
281 [self lockFocus]; // in case something tries to draw from here
282 [self prepareContext];
284 /* I considered just not even calling the free callback at all...
285 But webcollage-cocoa needs it, to kill the inferior webcollage
286 processes (since the screen saver framework never generates a
287 SIGPIPE for them...) Instead, I turned off the free call in
288 xlockmore.c, which is where all of the bogus calls are anyway.
290 xsft->free_cb (xdpy, xwindow, xdata);
293 // setup_p = NO; // #### wait, do we need this?
298 [super stopAnimation];
302 /* Hook for the XScreenSaverGLView subclass
304 - (void) prepareContext
308 /* Hook for the XScreenSaverGLView subclass
310 - (void) resizeContext
316 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
318 fps_compute (fpst, 0);
323 - (void) animateOneFrame
328 xdpy = jwxyz_make_display (self);
329 xwindow = XRootWindow (xdpy, 0);
335 xsft->setup_cb (xsft, xsft->setup_arg);
339 NSAssert(!xdata, @"xdata already initialized");
344 XSetWindowBackground (xdpy, xwindow,
345 get_pixel_resource (xdpy, 0,
346 "background", "Background"));
347 XClearWindow (xdpy, xwindow);
349 [[self window] setAcceptsMouseMovedEvents:YES];
351 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
352 drawing primitives will run on the GPU instead of the CPU.
353 It seems like it might make things worse rather than better,
354 though... Plus it makes us binary-incompatible with 10.4.
356 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
357 [[self window] setPreferredBackingLocation:
358 NSWindowBackingLocationVideoMemory];
362 /* Kludge: even though the init_cb functions are declared to take 2 args,
363 actually call them with 3, for the benefit of xlockmore_init() and
366 void *(*init_cb) (Display *, Window, void *) =
367 (void *(*) (Display *, Window, void *)) xsft->init_cb;
369 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
371 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
372 fpst = fps_init (xdpy, xwindow);
373 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
377 /* I don't understand why we have to do this *every frame*, but we do,
378 or else the cursor comes back on.
380 if (![self isPreview])
381 [NSCursor setHiddenUntilMouseMoves:YES];
386 /* This is just a guess, but the -fps code wants to know how long
387 we were sleeping between frames.
389 unsigned long usecs = 1000000 * [self animationTimeInterval];
390 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
391 fps_slept (fpst, usecs);
395 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
396 This is bad, because some of the screen hacks want to delay for long
397 periods (like 5 seconds or a minute!) between frames, and running them
398 all at 60 FPS is no good.
400 So, we don't use setAnimationTimeInterval, and just let the framework call
401 us whenever. But, we only invoke the screen hack's "draw frame" method
402 when enough time has expired.
404 This means two extra calls to gettimeofday() per frame. For fast-cycling
405 screen savers, that might actually slow them down. Oh well.
407 #### Also, we do not run the draw callback faster than the system's
408 animationTimeInterval, so if any savers are pickier about timing
409 than that, this may slow them down too much. If that's a problem,
410 then we could call draw_cb in a loop here (with usleep) until the
411 next call would put us past animationTimeInterval... But a better
412 approach would probably be to just change the saver to not do that.
415 gettimeofday (&tv, 0);
416 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
417 if (now < next_frame_time) return;
419 [self prepareContext];
422 // We do this here instead of in setFrameSize so that all the
423 // Xlib drawing takes place under the animation timer.
424 [self resizeContext];
425 NSRect r = [self frame];
426 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
430 // Run any XtAppAddInput callbacks now.
431 // (Note that XtAppAddTimeOut callbacks have already been run by
432 // the Cocoa event loop.)
434 jwxyz_sources_run (display_sources_data (xdpy));
438 NSDisableScreenUpdates();
439 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
440 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
442 NSEnableScreenUpdates();
444 gettimeofday (&tv, 0);
445 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
446 next_frame_time = now + (delay / 1000000.0);
449 # ifdef DO_GC_HACKERY
450 /* Current theory is that the 10.6 garbage collector sucks in the
453 It only does a collection when a threshold of outstanding
454 collectable allocations has been surpassed. However, CoreGraphics
455 creates lots of small collectable allocations that contain pointers
456 to very large non-collectable allocations: a small CG object that's
457 collectable referencing large malloc'd allocations (non-collectable)
458 containing bitmap data. So the large allocation doesn't get freed
459 until GC collects the small allocation, which triggers its finalizer
460 to run which frees the large allocation. So GC is deciding that it
461 doesn't really need to run, even though the process has gotten
462 enormous. GC eventually runs once pageouts have happened, but by
463 then it's too late, and the machine's resident set has been
466 So, we force an exhaustive garbage collection in this process
467 approximately every 5 seconds whether the system thinks it needs
474 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
477 # endif // DO_GC_HACKERY
481 - (void)drawRect:(NSRect)rect
483 if (xwindow) // clear to the X window's bg color, not necessarily black.
484 XClearWindow (xdpy, xwindow);
486 [super drawRect:rect]; // early: black.
490 - (void) setFrameSize:(NSSize) newSize
492 [super setFrameSize:newSize];
493 if ([self isAnimating]) {
498 - (void) setFrame:(NSRect) newRect
500 [super setFrame:newRect];
501 if (xwindow) // inform Xlib that the window has changed.
502 jwxyz_window_resized (xdpy, xwindow);
506 +(BOOL) performGammaFade
511 - (BOOL) hasConfigureSheet
516 - (NSWindow *) configureSheet
518 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
519 NSString *file = [NSString stringWithCString:xsft->progclass
520 encoding:NSUTF8StringEncoding];
521 file = [file lowercaseString];
522 NSString *path = [bundle pathForResource:file ofType:@"xml"];
524 NSLog (@"%@.xml does not exist in the application bundle: %@/",
525 file, [bundle resourcePath]);
529 NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
531 options:xsft->options
532 controller:[prefsReader userDefaultsController]];
534 // #### am I expected to retain this, or not? wtf.
535 // I thought not, but if I don't do this, we (sometimes) crash.
542 /* Announce our willingness to accept keyboard input.
544 - (BOOL)acceptsFirstResponder
550 /* Convert an NSEvent into an XEvent, and pass it along.
551 Returns YES if it was handled.
553 - (BOOL) doEvent: (NSEvent *) e
556 if (![self isPreview] || // no event handling if actually screen-saving!
557 ![self isAnimating] ||
562 memset (&xe, 0, sizeof(xe));
566 int flags = [e modifierFlags];
567 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
568 if (flags & NSShiftKeyMask) state |= ShiftMask;
569 if (flags & NSControlKeyMask) state |= ControlMask;
570 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
571 if (flags & NSCommandKeyMask) state |= Mod2Mask;
573 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
576 int y = [self frame].size.height - p.y;
584 xe.xbutton.state = state;
585 if ([e type] == NSScrollWheel)
586 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
587 [e deltaY] < 0 ? Button5 :
588 [e deltaX] > 0 ? Button6 :
589 [e deltaX] < 0 ? Button7 :
592 xe.xbutton.button = [e buttonNumber] + 1;
597 xe.xmotion.state = state;
602 NSString *nss = [e characters];
603 const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding];
604 xe.xkey.keycode = (s && *s ? *s : 0);
605 xe.xkey.state = state;
613 [self prepareContext];
614 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
620 - (void) mouseDown: (NSEvent *) e
622 if (! [self doEvent:e type:ButtonPress])
626 - (void) mouseUp: (NSEvent *) e
628 if (! [self doEvent:e type:ButtonRelease])
632 - (void) otherMouseDown: (NSEvent *) e
634 if (! [self doEvent:e type:ButtonPress])
635 [super otherMouseDown:e];
638 - (void) otherMouseUp: (NSEvent *) e
640 if (! [self doEvent:e type:ButtonRelease])
641 [super otherMouseUp:e];
644 - (void) mouseMoved: (NSEvent *) e
646 if (! [self doEvent:e type:MotionNotify])
647 [super mouseMoved:e];
650 - (void) mouseDragged: (NSEvent *) e
652 if (! [self doEvent:e type:MotionNotify])
653 [super mouseDragged:e];
656 - (void) otherMouseDragged: (NSEvent *) e
658 if (! [self doEvent:e type:MotionNotify])
659 [super otherMouseDragged:e];
662 - (void) scrollWheel: (NSEvent *) e
664 if (! [self doEvent:e type:ButtonPress])
665 [super scrollWheel:e];
668 - (void) keyDown: (NSEvent *) e
670 if (! [self doEvent:e type:KeyPress])
674 - (void) keyUp: (NSEvent *) e
676 if (! [self doEvent:e type:KeyRelease])
683 /* Utility functions...
687 get_prefsReader (Display *dpy)
689 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
691 return [view prefsReader];
696 get_string_resource (Display *dpy, char *name, char *class)
698 return [get_prefsReader(dpy) getStringResource:name];
702 get_boolean_resource (Display *dpy, char *name, char *class)
704 return [get_prefsReader(dpy) getBooleanResource:name];
708 get_integer_resource (Display *dpy, char *name, char *class)
710 return [get_prefsReader(dpy) getIntegerResource:name];
714 get_float_resource (Display *dpy, char *name, char *class)
716 return [get_prefsReader(dpy) getFloatResource:name];