1 /* xscreensaver, Copyright (c) 2006, 2007 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 extern struct xscreensaver_function_table *xscreensaver_function_table;
26 /* Global variables used by the screen savers
29 const char *progclass;
33 @implementation XScreenSaverView
35 - (struct xscreensaver_function_table *) findFunctionTable
37 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
38 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
40 NSString *path = [nsb bundlePath];
41 NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension]
43 NSString *suffix = @"_xscreensaver_function_table";
44 NSString *table_name = [name stringByAppendingString:suffix];
46 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
50 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
52 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
54 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
55 NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path);
57 // NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr);
58 return (struct xscreensaver_function_table *) addr;
62 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
63 // to $PATH for the benefit of savers that include helper shell scripts.
67 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
68 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
70 NSString *nsdir = [nsb resourcePath];
71 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
72 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
73 const char *opath = getenv ("PATH");
74 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
75 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
76 strcpy (npath, "PATH=");
79 strcat (npath, opath);
88 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
89 // (e.g., "xscreensaver-text") know how to look up resources.
91 - (void) setResourcesEnv:(NSString *) name
93 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
94 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
96 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
97 char *env = (char *) malloc (strlen (s) + 40);
98 strcpy (env, "XSCREENSAVER_CLASSPATH=");
109 add_default_options (const XrmOptionDescRec *opts,
110 const char * const *defs,
111 XrmOptionDescRec **opts_ret,
112 const char ***defs_ret)
114 /* These aren't "real" command-line options (there are no actual command-line
115 options in the Cocoa version); but this is the somewhat kludgey way that
116 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
117 ../hacks/config/*.xml files communicate with the preferences database.
119 static const XrmOptionDescRec default_options [] = {
120 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
121 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
122 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
123 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
124 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
125 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
126 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
127 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
128 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
131 static const char *default_defaults [] = {
136 ".grabDesktopImages: yes",
137 ".chooseRandomImages: no",
138 ".imageDirectory: ~/Pictures",
143 for (i = 0; default_options[i].option; i++)
145 for (i = 0; opts[i].option; i++)
148 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
149 calloc (count + 1, sizeof (*opts2));
153 while (default_options[j].option) {
154 opts2[i] = default_options[j];
158 while (opts[j].option) {
169 for (i = 0; default_defaults[i]; i++)
171 for (i = 0; defs[i]; i++)
174 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
178 while (default_defaults[j]) {
179 defs2[i] = default_defaults[j];
192 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
194 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
197 xsft = [self findFunctionTable];
202 xsft->setup_cb (xsft, xsft->setup_arg);
204 /* The plist files for these preferences show up in
205 $HOME/Library/Preferences/ByHost/ in a file named like
206 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
208 NSString *name = [NSString stringWithCString:xsft->progclass
209 encoding:NSUTF8StringEncoding];
210 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
211 [self setResourcesEnv:name];
214 XrmOptionDescRec *opts = 0;
215 const char **defs = 0;
216 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
217 prefsReader = [[PrefsReader alloc]
218 initWithName:name xrmKeys:opts defaults:defs];
220 // free (opts); // bah, we need these! #### leak!
221 xsft->options = opts;
223 progname = progclass = xsft->progclass;
232 NSAssert(![self isAnimating], @"still animating");
233 NSAssert(!xdata, @"xdata not yet freed");
235 jwxyz_free_display (xdpy);
236 [prefsReader release];
240 - (PrefsReader *) prefsReader
246 - (void) startAnimation
248 NSAssert(![self isAnimating], @"already animating");
249 NSAssert(!initted_p && !xdata, @"already initialized");
250 [super startAnimation];
251 /* We can't draw on the window from this method, so we actually do the
252 initialization of the screen saver (xsft->init_cb) in the first call
253 to animateOneFrame() instead.
257 - (void)stopAnimation
259 NSAssert([self isAnimating], @"not animating");
263 [self lockFocus]; // in case something tries to draw from here
264 [self prepareContext];
265 xsft->free_cb (xdpy, xwindow, xdata);
268 // setup_p = NO; // #### wait, do we need this?
273 [super stopAnimation];
277 /* Hook for the XScreenSaverGLView subclass
279 - (void) prepareContext
283 /* Hook for the XScreenSaverGLView subclass
285 - (void) resizeContext
289 - (void) animateOneFrame
294 xdpy = jwxyz_make_display (self);
295 xwindow = XRootWindow (xdpy, 0);
301 xsft->setup_cb (xsft, xsft->setup_arg);
305 NSAssert(!xdata, @"xdata already initialized");
310 XSetWindowBackground (xdpy, xwindow,
311 get_pixel_resource (xdpy, 0,
312 "background", "Background"));
313 XClearWindow (xdpy, xwindow);
315 [[self window] setAcceptsMouseMovedEvents:YES];
317 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
318 drawing primitives will run on the GPU instead of the CPU.
319 It seems like it might make things worse rather than better,
320 though... Plus it makes us binary-incompatible with 10.4.
322 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
323 [[self window] setPreferredBackingLocation:
324 NSWindowBackingLocationVideoMemory];
328 /* Kludge: even though the init_cb functions are declared to take 2 args,
329 actually call them with 3, for the benefit of xlockmore_init() and
332 void *(*init_cb) (Display *, Window, void *) =
333 (void *(*) (Display *, Window, void *)) xsft->init_cb;
335 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
338 /* I don't understand why we have to do this *every frame*, but we do,
339 or else the cursor comes back on.
341 if (![self isPreview])
342 [NSCursor setHiddenUntilMouseMoves:YES];
344 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
345 This is bad, because some of the screen hacks want to delay for long
346 periods (like 5 seconds or a minute!) between frames, and running them
347 all at 60 FPS is no good.
349 So, we don't use setAnimationTimeInterval, and just let the framework call
350 us whenever. But, we only invoke the screen hack's "draw frame" method
351 when enough time has expired.
353 This means two extra calls to gettimeofday() per frame. For fast-cycling
354 screen savers, that might actually slow them down. Oh well.
356 #### Also, we do not run the draw callback faster than the system's
357 animationTimeInterval, so if any savers are pickier about timing
358 than that, this may slow them down too much. If that's a problem,
359 then we could call draw_cb in a loop here (with usleep) until the
360 next call would put us past animationTimeInterval... But a better
361 approach would probably be to just change the saver to not do that.
364 gettimeofday (&tv, 0);
365 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
366 if (now < next_frame_time) return;
368 [self prepareContext];
371 // We do this here instead of in setFrameSize so that all the
372 // Xlib drawing takes place under the animation timer.
373 [self resizeContext];
374 NSRect r = [self frame];
375 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
379 // Run any XtAppAddInput callbacks now.
380 // (Note that XtAppAddTimeOut callbacks have already been run by
381 // the Cocoa event loop.)
383 jwxyz_sources_run (display_sources_data (xdpy));
387 NSDisableScreenUpdates();
388 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
390 NSEnableScreenUpdates();
392 gettimeofday (&tv, 0);
393 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
394 next_frame_time = now + (delay / 1000000.0);
398 - (void)drawRect:(NSRect)rect
400 if (xwindow) // clear to the X window's bg color, not necessarily black.
401 XClearWindow (xdpy, xwindow);
403 [super drawRect:rect]; // early: black.
407 - (void) setFrameSize:(NSSize) newSize
409 [super setFrameSize:newSize];
410 if ([self isAnimating]) {
415 - (void) setFrame:(NSRect) newRect
417 [super setFrame:newRect];
418 if (xwindow) // inform Xlib that the window has changed.
419 jwxyz_window_resized (xdpy, xwindow);
423 +(BOOL) performGammaFade
428 - (BOOL) hasConfigureSheet
433 - (NSWindow *) configureSheet
435 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
436 NSString *file = [NSString stringWithCString:xsft->progclass
437 encoding:NSUTF8StringEncoding];
438 file = [file lowercaseString];
439 NSString *path = [bundle pathForResource:file ofType:@"xml"];
441 NSLog (@"%@.xml does not exist in the application bundle: %@/",
442 file, [bundle resourcePath]);
446 NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
448 options:xsft->options
449 controller:[prefsReader userDefaultsController]];
451 // #### am I expected to retain this, or not? wtf.
452 // I thought not, but if I don't do this, we (sometimes) crash.
459 /* Announce our willingness to accept keyboard input.
461 - (BOOL)acceptsFirstResponder
467 /* Convert an NSEvent into an XEvent, and pass it along.
468 Returns YES if it was handled.
470 - (BOOL) doEvent: (NSEvent *) e
473 if (![self isPreview] || // no event handling if actually screen-saving!
474 ![self isAnimating] ||
479 memset (&xe, 0, sizeof(xe));
483 int flags = [e modifierFlags];
484 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
485 if (flags & NSShiftKeyMask) state |= ShiftMask;
486 if (flags & NSControlKeyMask) state |= ControlMask;
487 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
488 if (flags & NSCommandKeyMask) state |= Mod2Mask;
490 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
493 int y = [self frame].size.height - p.y;
501 xe.xbutton.state = state;
502 if ([e type] == NSScrollWheel)
503 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
504 [e deltaY] < 0 ? Button5 :
505 [e deltaX] > 0 ? Button6 :
506 [e deltaX] < 0 ? Button7 :
509 xe.xbutton.button = [e buttonNumber] + 1;
514 xe.xmotion.state = state;
519 NSString *nss = [e characters];
520 const char *s = [nss cStringUsingEncoding:NSUTF8StringEncoding];
521 xe.xkey.keycode = (s && *s ? *s : 0);
522 xe.xkey.state = state;
530 [self prepareContext];
531 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
537 - (void) mouseDown: (NSEvent *) e
539 if (! [self doEvent:e type:ButtonPress])
543 - (void) mouseUp: (NSEvent *) e
545 if (! [self doEvent:e type:ButtonRelease])
549 - (void) otherMouseDown: (NSEvent *) e
551 if (! [self doEvent:e type:ButtonPress])
552 [super otherMouseDown:e];
555 - (void) otherMouseUp: (NSEvent *) e
557 if (! [self doEvent:e type:ButtonRelease])
558 [super otherMouseUp:e];
561 - (void) mouseMoved: (NSEvent *) e
563 if (! [self doEvent:e type:MotionNotify])
564 [super mouseMoved:e];
567 - (void) mouseDragged: (NSEvent *) e
569 if (! [self doEvent:e type:MotionNotify])
570 [super mouseDragged:e];
573 - (void) otherMouseDragged: (NSEvent *) e
575 if (! [self doEvent:e type:MotionNotify])
576 [super otherMouseDragged:e];
579 - (void) scrollWheel: (NSEvent *) e
581 if (! [self doEvent:e type:ButtonPress])
582 [super scrollWheel:e];
585 - (void) keyDown: (NSEvent *) e
587 if (! [self doEvent:e type:KeyPress])
591 - (void) keyUp: (NSEvent *) e
593 if (! [self doEvent:e type:KeyRelease])
600 /* Utility functions...
604 get_prefsReader (Display *dpy)
606 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
608 return [view prefsReader];
613 get_string_resource (Display *dpy, char *name, char *class)
615 return [get_prefsReader(dpy) getStringResource:name];
619 get_boolean_resource (Display *dpy, char *name, char *class)
621 return [get_prefsReader(dpy) getBooleanResource:name];
625 get_integer_resource (Display *dpy, char *name, char *class)
627 return [get_prefsReader(dpy) getIntegerResource:name];
631 get_float_resource (Display *dpy, char *name, char *class)
633 return [get_prefsReader(dpy) getFloatResource:name];