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 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
138 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
139 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
140 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
141 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
142 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
143 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
144 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
147 static const char *default_defaults [] = {
149 ".doubleBuffer: True",
150 ".multiSample: False",
154 ".textURL: http://twitter.com/statuses/public_timeline.atom",
156 ".grabDesktopImages: yes",
157 ".chooseRandomImages: no",
158 ".imageDirectory: ~/Pictures",
163 for (i = 0; default_options[i].option; i++)
165 for (i = 0; opts[i].option; i++)
168 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
169 calloc (count + 1, sizeof (*opts2));
173 while (default_options[j].option) {
174 opts2[i] = default_options[j];
178 while (opts[j].option) {
189 for (i = 0; default_defaults[i]; i++)
191 for (i = 0; defs[i]; i++)
194 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
198 while (default_defaults[j]) {
199 defs2[i] = default_defaults[j];
212 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
214 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
217 xsft = [self findFunctionTable];
222 xsft->setup_cb (xsft, xsft->setup_arg);
224 /* The plist files for these preferences show up in
225 $HOME/Library/Preferences/ByHost/ in a file named like
226 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
228 NSString *name = [NSString stringWithCString:xsft->progclass
229 encoding:NSUTF8StringEncoding];
230 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
231 [self setResourcesEnv:name];
234 XrmOptionDescRec *opts = 0;
235 const char **defs = 0;
236 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
237 prefsReader = [[PrefsReader alloc]
238 initWithName:name xrmKeys:opts defaults:defs];
240 // free (opts); // bah, we need these! #### leak!
241 xsft->options = opts;
243 progname = progclass = xsft->progclass;
252 NSAssert(![self isAnimating], @"still animating");
253 NSAssert(!xdata, @"xdata not yet freed");
255 jwxyz_free_display (xdpy);
256 [prefsReader release];
260 - (PrefsReader *) prefsReader
266 - (void) startAnimation
268 NSAssert(![self isAnimating], @"already animating");
269 NSAssert(!initted_p && !xdata, @"already initialized");
270 [super startAnimation];
271 /* We can't draw on the window from this method, so we actually do the
272 initialization of the screen saver (xsft->init_cb) in the first call
273 to animateOneFrame() instead.
277 - (void)stopAnimation
279 NSAssert([self isAnimating], @"not animating");
283 [self lockFocus]; // in case something tries to draw from here
284 [self prepareContext];
286 /* I considered just not even calling the free callback at all...
287 But webcollage-cocoa needs it, to kill the inferior webcollage
288 processes (since the screen saver framework never generates a
289 SIGPIPE for them...) Instead, I turned off the free call in
290 xlockmore.c, which is where all of the bogus calls are anyway.
292 xsft->free_cb (xdpy, xwindow, xdata);
295 // setup_p = NO; // #### wait, do we need this?
300 [super stopAnimation];
304 /* Hook for the XScreenSaverGLView subclass
306 - (void) prepareContext
310 /* Hook for the XScreenSaverGLView subclass
312 - (void) resizeContext
318 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
320 fps_compute (fpst, 0, -1);
325 - (void) animateOneFrame
330 xdpy = jwxyz_make_display (self);
331 xwindow = XRootWindow (xdpy, 0);
337 xsft->setup_cb (xsft, xsft->setup_arg);
341 NSAssert(!xdata, @"xdata already initialized");
346 XSetWindowBackground (xdpy, xwindow,
347 get_pixel_resource (xdpy, 0,
348 "background", "Background"));
349 XClearWindow (xdpy, xwindow);
351 [[self window] setAcceptsMouseMovedEvents:YES];
353 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
354 drawing primitives will run on the GPU instead of the CPU.
355 It seems like it might make things worse rather than better,
356 though... Plus it makes us binary-incompatible with 10.4.
358 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
359 [[self window] setPreferredBackingLocation:
360 NSWindowBackingLocationVideoMemory];
364 /* Kludge: even though the init_cb functions are declared to take 2 args,
365 actually call them with 3, for the benefit of xlockmore_init() and
368 void *(*init_cb) (Display *, Window, void *) =
369 (void *(*) (Display *, Window, void *)) xsft->init_cb;
371 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
373 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
374 fpst = fps_init (xdpy, xwindow);
375 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
379 /* I don't understand why we have to do this *every frame*, but we do,
380 or else the cursor comes back on.
382 if (![self isPreview])
383 [NSCursor setHiddenUntilMouseMoves:YES];
388 /* This is just a guess, but the -fps code wants to know how long
389 we were sleeping between frames.
391 unsigned long usecs = 1000000 * [self animationTimeInterval];
392 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
393 fps_slept (fpst, usecs);
397 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
398 This is bad, because some of the screen hacks want to delay for long
399 periods (like 5 seconds or a minute!) between frames, and running them
400 all at 60 FPS is no good.
402 So, we don't use setAnimationTimeInterval, and just let the framework call
403 us whenever. But, we only invoke the screen hack's "draw frame" method
404 when enough time has expired.
406 This means two extra calls to gettimeofday() per frame. For fast-cycling
407 screen savers, that might actually slow them down. Oh well.
409 #### Also, we do not run the draw callback faster than the system's
410 animationTimeInterval, so if any savers are pickier about timing
411 than that, this may slow them down too much. If that's a problem,
412 then we could call draw_cb in a loop here (with usleep) until the
413 next call would put us past animationTimeInterval... But a better
414 approach would probably be to just change the saver to not do that.
417 gettimeofday (&tv, 0);
418 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
419 if (now < next_frame_time) return;
421 [self prepareContext];
424 // We do this here instead of in setFrameSize so that all the
425 // Xlib drawing takes place under the animation timer.
426 [self resizeContext];
427 NSRect r = [self frame];
428 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
432 // Run any XtAppAddInput callbacks now.
433 // (Note that XtAppAddTimeOut callbacks have already been run by
434 // the Cocoa event loop.)
436 jwxyz_sources_run (display_sources_data (xdpy));
440 NSDisableScreenUpdates();
441 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
442 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
444 NSEnableScreenUpdates();
446 gettimeofday (&tv, 0);
447 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
448 next_frame_time = now + (delay / 1000000.0);
451 # ifdef DO_GC_HACKERY
452 /* Current theory is that the 10.6 garbage collector sucks in the
455 It only does a collection when a threshold of outstanding
456 collectable allocations has been surpassed. However, CoreGraphics
457 creates lots of small collectable allocations that contain pointers
458 to very large non-collectable allocations: a small CG object that's
459 collectable referencing large malloc'd allocations (non-collectable)
460 containing bitmap data. So the large allocation doesn't get freed
461 until GC collects the small allocation, which triggers its finalizer
462 to run which frees the large allocation. So GC is deciding that it
463 doesn't really need to run, even though the process has gotten
464 enormous. GC eventually runs once pageouts have happened, but by
465 then it's too late, and the machine's resident set has been
468 So, we force an exhaustive garbage collection in this process
469 approximately every 5 seconds whether the system thinks it needs
476 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
479 # endif // DO_GC_HACKERY
483 - (void)drawRect:(NSRect)rect
485 if (xwindow) // clear to the X window's bg color, not necessarily black.
486 XClearWindow (xdpy, xwindow);
488 [super drawRect:rect]; // early: black.
492 - (void) setFrameSize:(NSSize) newSize
494 [super setFrameSize:newSize];
495 if ([self isAnimating]) {
500 - (void) setFrame:(NSRect) newRect
502 [super setFrame:newRect];
503 if (xwindow) // inform Xlib that the window has changed.
504 jwxyz_window_resized (xdpy, xwindow);
508 +(BOOL) performGammaFade
513 - (BOOL) hasConfigureSheet
518 - (NSWindow *) configureSheet
520 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
521 NSString *file = [NSString stringWithCString:xsft->progclass
522 encoding:NSUTF8StringEncoding];
523 file = [file lowercaseString];
524 NSString *path = [bundle pathForResource:file ofType:@"xml"];
526 NSLog (@"%@.xml does not exist in the application bundle: %@/",
527 file, [bundle resourcePath]);
531 NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
533 options:xsft->options
534 controller:[prefsReader userDefaultsController]];
536 // #### am I expected to retain this, or not? wtf.
537 // I thought not, but if I don't do this, we (sometimes) crash.
544 - (NSUserDefaultsController *) userDefaultsController
546 return [prefsReader userDefaultsController];
550 /* Announce our willingness to accept keyboard input.
552 - (BOOL)acceptsFirstResponder
558 /* Convert an NSEvent into an XEvent, and pass it along.
559 Returns YES if it was handled.
561 - (BOOL) doEvent: (NSEvent *) e
564 if (![self isPreview] || // no event handling if actually screen-saving!
565 ![self isAnimating] ||
570 memset (&xe, 0, sizeof(xe));
574 int flags = [e modifierFlags];
575 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
576 if (flags & NSShiftKeyMask) state |= ShiftMask;
577 if (flags & NSControlKeyMask) state |= ControlMask;
578 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
579 if (flags & NSCommandKeyMask) state |= Mod2Mask;
581 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
584 int y = [self frame].size.height - p.y;
592 xe.xbutton.state = state;
593 if ([e type] == NSScrollWheel)
594 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
595 [e deltaY] < 0 ? Button5 :
596 [e deltaX] > 0 ? Button6 :
597 [e deltaX] < 0 ? Button7 :
600 xe.xbutton.button = [e buttonNumber] + 1;
605 xe.xmotion.state = state;
610 NSString *nss = [e characters];
611 const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding];
612 xe.xkey.keycode = (s && *s ? *s : 0);
613 xe.xkey.state = state;
621 [self prepareContext];
622 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
628 - (void) mouseDown: (NSEvent *) e
630 if (! [self doEvent:e type:ButtonPress])
634 - (void) mouseUp: (NSEvent *) e
636 if (! [self doEvent:e type:ButtonRelease])
640 - (void) otherMouseDown: (NSEvent *) e
642 if (! [self doEvent:e type:ButtonPress])
643 [super otherMouseDown:e];
646 - (void) otherMouseUp: (NSEvent *) e
648 if (! [self doEvent:e type:ButtonRelease])
649 [super otherMouseUp:e];
652 - (void) mouseMoved: (NSEvent *) e
654 if (! [self doEvent:e type:MotionNotify])
655 [super mouseMoved:e];
658 - (void) mouseDragged: (NSEvent *) e
660 if (! [self doEvent:e type:MotionNotify])
661 [super mouseDragged:e];
664 - (void) otherMouseDragged: (NSEvent *) e
666 if (! [self doEvent:e type:MotionNotify])
667 [super otherMouseDragged:e];
670 - (void) scrollWheel: (NSEvent *) e
672 if (! [self doEvent:e type:ButtonPress])
673 [super scrollWheel:e];
676 - (void) keyDown: (NSEvent *) e
678 if (! [self doEvent:e type:KeyPress])
682 - (void) keyUp: (NSEvent *) e
684 if (! [self doEvent:e type:KeyRelease])
691 /* Utility functions...
695 get_prefsReader (Display *dpy)
697 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
699 return [view prefsReader];
704 get_string_resource (Display *dpy, char *name, char *class)
706 return [get_prefsReader(dpy) getStringResource:name];
710 get_boolean_resource (Display *dpy, char *name, char *class)
712 return [get_prefsReader(dpy) getBooleanResource:name];
716 get_integer_resource (Display *dpy, char *name, char *class)
718 return [get_prefsReader(dpy) getIntegerResource:name];
722 get_float_resource (Display *dpy, char *name, char *class)
724 return [get_prefsReader(dpy) getFloatResource:name];