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);
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", // for most OpenGL hacks
153 ".grabDesktopImages: yes",
154 ".chooseRandomImages: no",
155 ".imageDirectory: ~/Pictures",
160 for (i = 0; default_options[i].option; i++)
162 for (i = 0; opts[i].option; i++)
165 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
166 calloc (count + 1, sizeof (*opts2));
170 while (default_options[j].option) {
171 opts2[i] = default_options[j];
175 while (opts[j].option) {
186 for (i = 0; default_defaults[i]; i++)
188 for (i = 0; defs[i]; i++)
191 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
195 while (default_defaults[j]) {
196 defs2[i] = default_defaults[j];
209 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
211 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
214 xsft = [self findFunctionTable];
219 xsft->setup_cb (xsft, xsft->setup_arg);
221 /* The plist files for these preferences show up in
222 $HOME/Library/Preferences/ByHost/ in a file named like
223 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
225 NSString *name = [NSString stringWithCString:xsft->progclass
226 encoding:NSUTF8StringEncoding];
227 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
228 [self setResourcesEnv:name];
231 XrmOptionDescRec *opts = 0;
232 const char **defs = 0;
233 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
234 prefsReader = [[PrefsReader alloc]
235 initWithName:name xrmKeys:opts defaults:defs];
237 // free (opts); // bah, we need these! #### leak!
238 xsft->options = opts;
240 progname = progclass = xsft->progclass;
249 NSAssert(![self isAnimating], @"still animating");
250 NSAssert(!xdata, @"xdata not yet freed");
252 jwxyz_free_display (xdpy);
253 [prefsReader release];
257 - (PrefsReader *) prefsReader
263 - (void) startAnimation
265 NSAssert(![self isAnimating], @"already animating");
266 NSAssert(!initted_p && !xdata, @"already initialized");
267 [super startAnimation];
268 /* We can't draw on the window from this method, so we actually do the
269 initialization of the screen saver (xsft->init_cb) in the first call
270 to animateOneFrame() instead.
274 - (void)stopAnimation
276 NSAssert([self isAnimating], @"not animating");
280 [self lockFocus]; // in case something tries to draw from here
281 [self prepareContext];
283 /* I considered just not even calling the free callback at all...
284 But webcollage-cocoa needs it, to kill the inferior webcollage
285 processes (since the screen saver framework never generates a
286 SIGPIPE for them...) Instead, I turned off the free call in
287 xlockmore.c, which is where all of the bogus calls are anyway.
289 xsft->free_cb (xdpy, xwindow, xdata);
292 // setup_p = NO; // #### wait, do we need this?
297 [super stopAnimation];
301 /* Hook for the XScreenSaverGLView subclass
303 - (void) prepareContext
307 /* Hook for the XScreenSaverGLView subclass
309 - (void) resizeContext
315 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
317 fps_compute (fpst, 0);
322 - (void) animateOneFrame
327 xdpy = jwxyz_make_display (self);
328 xwindow = XRootWindow (xdpy, 0);
334 xsft->setup_cb (xsft, xsft->setup_arg);
338 NSAssert(!xdata, @"xdata already initialized");
343 XSetWindowBackground (xdpy, xwindow,
344 get_pixel_resource (xdpy, 0,
345 "background", "Background"));
346 XClearWindow (xdpy, xwindow);
348 [[self window] setAcceptsMouseMovedEvents:YES];
350 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
351 drawing primitives will run on the GPU instead of the CPU.
352 It seems like it might make things worse rather than better,
353 though... Plus it makes us binary-incompatible with 10.4.
355 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
356 [[self window] setPreferredBackingLocation:
357 NSWindowBackingLocationVideoMemory];
361 /* Kludge: even though the init_cb functions are declared to take 2 args,
362 actually call them with 3, for the benefit of xlockmore_init() and
365 void *(*init_cb) (Display *, Window, void *) =
366 (void *(*) (Display *, Window, void *)) xsft->init_cb;
368 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
370 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
371 fpst = fps_init (xdpy, xwindow);
372 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
376 /* I don't understand why we have to do this *every frame*, but we do,
377 or else the cursor comes back on.
379 if (![self isPreview])
380 [NSCursor setHiddenUntilMouseMoves:YES];
385 /* This is just a guess, but the -fps code wants to know how long
386 we were sleeping between frames.
388 unsigned long usecs = 1000000 * [self animationTimeInterval];
389 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
390 fps_slept (fpst, usecs);
394 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
395 This is bad, because some of the screen hacks want to delay for long
396 periods (like 5 seconds or a minute!) between frames, and running them
397 all at 60 FPS is no good.
399 So, we don't use setAnimationTimeInterval, and just let the framework call
400 us whenever. But, we only invoke the screen hack's "draw frame" method
401 when enough time has expired.
403 This means two extra calls to gettimeofday() per frame. For fast-cycling
404 screen savers, that might actually slow them down. Oh well.
406 #### Also, we do not run the draw callback faster than the system's
407 animationTimeInterval, so if any savers are pickier about timing
408 than that, this may slow them down too much. If that's a problem,
409 then we could call draw_cb in a loop here (with usleep) until the
410 next call would put us past animationTimeInterval... But a better
411 approach would probably be to just change the saver to not do that.
414 gettimeofday (&tv, 0);
415 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
416 if (now < next_frame_time) return;
418 [self prepareContext];
421 // We do this here instead of in setFrameSize so that all the
422 // Xlib drawing takes place under the animation timer.
423 [self resizeContext];
424 NSRect r = [self frame];
425 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
429 // Run any XtAppAddInput callbacks now.
430 // (Note that XtAppAddTimeOut callbacks have already been run by
431 // the Cocoa event loop.)
433 jwxyz_sources_run (display_sources_data (xdpy));
437 NSDisableScreenUpdates();
438 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
439 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
441 NSEnableScreenUpdates();
443 gettimeofday (&tv, 0);
444 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
445 next_frame_time = now + (delay / 1000000.0);
448 # ifdef DO_GC_HACKERY
449 /* Current theory is that the 10.6 garbage collector sucks in the
452 It only does a collection when a threshold of outstanding
453 collectable allocations has been surpassed. However, CoreGraphics
454 creates lots of small collectable allocations that contain pointers
455 to very large non-collectable allocations: a small CG object that's
456 collectable referencing large malloc'd allocations (non-collectable)
457 containing bitmap data. So the large allocation doesn't get freed
458 until GC collects the small allocation, which triggers its finalizer
459 to run which frees the large allocation. So GC is deciding that it
460 doesn't really need to run, even though the process has gotten
461 enormous. GC eventually runs once pageouts have happened, but by
462 then it's too late, and the machine's resident set has been
465 So, we force an exhaustive garbage collection in this process
466 approximately every 5 seconds whether the system thinks it needs
473 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
476 # endif // DO_GC_HACKERY
480 - (void)drawRect:(NSRect)rect
482 if (xwindow) // clear to the X window's bg color, not necessarily black.
483 XClearWindow (xdpy, xwindow);
485 [super drawRect:rect]; // early: black.
489 - (void) setFrameSize:(NSSize) newSize
491 [super setFrameSize:newSize];
492 if ([self isAnimating]) {
497 - (void) setFrame:(NSRect) newRect
499 [super setFrame:newRect];
500 if (xwindow) // inform Xlib that the window has changed.
501 jwxyz_window_resized (xdpy, xwindow);
505 +(BOOL) performGammaFade
510 - (BOOL) hasConfigureSheet
515 - (NSWindow *) configureSheet
517 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
518 NSString *file = [NSString stringWithCString:xsft->progclass
519 encoding:NSUTF8StringEncoding];
520 file = [file lowercaseString];
521 NSString *path = [bundle pathForResource:file ofType:@"xml"];
523 NSLog (@"%@.xml does not exist in the application bundle: %@/",
524 file, [bundle resourcePath]);
528 NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
530 options:xsft->options
531 controller:[prefsReader userDefaultsController]];
533 // #### am I expected to retain this, or not? wtf.
534 // I thought not, but if I don't do this, we (sometimes) crash.
541 /* Announce our willingness to accept keyboard input.
543 - (BOOL)acceptsFirstResponder
549 /* Convert an NSEvent into an XEvent, and pass it along.
550 Returns YES if it was handled.
552 - (BOOL) doEvent: (NSEvent *) e
555 if (![self isPreview] || // no event handling if actually screen-saving!
556 ![self isAnimating] ||
561 memset (&xe, 0, sizeof(xe));
565 int flags = [e modifierFlags];
566 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
567 if (flags & NSShiftKeyMask) state |= ShiftMask;
568 if (flags & NSControlKeyMask) state |= ControlMask;
569 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
570 if (flags & NSCommandKeyMask) state |= Mod2Mask;
572 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
575 int y = [self frame].size.height - p.y;
583 xe.xbutton.state = state;
584 if ([e type] == NSScrollWheel)
585 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
586 [e deltaY] < 0 ? Button5 :
587 [e deltaX] > 0 ? Button6 :
588 [e deltaX] < 0 ? Button7 :
591 xe.xbutton.button = [e buttonNumber] + 1;
596 xe.xmotion.state = state;
601 NSString *nss = [e characters];
602 const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding];
603 xe.xkey.keycode = (s && *s ? *s : 0);
604 xe.xkey.state = state;
612 [self prepareContext];
613 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
619 - (void) mouseDown: (NSEvent *) e
621 if (! [self doEvent:e type:ButtonPress])
625 - (void) mouseUp: (NSEvent *) e
627 if (! [self doEvent:e type:ButtonRelease])
631 - (void) otherMouseDown: (NSEvent *) e
633 if (! [self doEvent:e type:ButtonPress])
634 [super otherMouseDown:e];
637 - (void) otherMouseUp: (NSEvent *) e
639 if (! [self doEvent:e type:ButtonRelease])
640 [super otherMouseUp:e];
643 - (void) mouseMoved: (NSEvent *) e
645 if (! [self doEvent:e type:MotionNotify])
646 [super mouseMoved:e];
649 - (void) mouseDragged: (NSEvent *) e
651 if (! [self doEvent:e type:MotionNotify])
652 [super mouseDragged:e];
655 - (void) otherMouseDragged: (NSEvent *) e
657 if (! [self doEvent:e type:MotionNotify])
658 [super otherMouseDragged:e];
661 - (void) scrollWheel: (NSEvent *) e
663 if (! [self doEvent:e type:ButtonPress])
664 [super scrollWheel:e];
667 - (void) keyDown: (NSEvent *) e
669 if (! [self doEvent:e type:KeyPress])
673 - (void) keyUp: (NSEvent *) e
675 if (! [self doEvent:e type:KeyRelease])
682 /* Utility functions...
686 get_prefsReader (Display *dpy)
688 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
690 return [view prefsReader];
695 get_string_resource (Display *dpy, char *name, char *class)
697 return [get_prefsReader(dpy) getStringResource:name];
701 get_boolean_resource (Display *dpy, char *name, char *class)
703 return [get_prefsReader(dpy) getBooleanResource:name];
707 get_integer_resource (Display *dpy, char *name, char *class)
709 return [get_prefsReader(dpy) getIntegerResource:name];
713 get_float_resource (Display *dpy, char *name, char *class)
715 return [get_prefsReader(dpy) getFloatResource:name];