http://www.jwz.org/xscreensaver/xscreensaver-5.09.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
1 /* xscreensaver, Copyright (c) 2006, 2007 Jamie Zawinski <jwz@jwz.org>
2 *
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 
9 * implied warranty.
10 */
11
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.
16  */
17
18 #import "XScreenSaverView.h"
19 #import "XScreenSaverConfigSheet.h"
20 #import "screenhackI.h"
21 #import "xlockmoreI.h"
22 #import "jwxyz-timers.h"
23
24 extern struct xscreensaver_function_table *xscreensaver_function_table;
25
26 /* Global variables used by the screen savers
27  */
28 const char *progname;
29 const char *progclass;
30 int mono_p = 0;
31
32
33 @implementation XScreenSaverView
34
35 - (struct xscreensaver_function_table *) findFunctionTable
36 {
37   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
38   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
39   
40   NSString *path = [nsb bundlePath];
41   NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension]
42                     lowercaseString];
43   NSString *suffix = @"_xscreensaver_function_table";
44   NSString *table_name = [name stringByAppendingString:suffix];
45   
46   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
47                                                (CFStringRef) path,
48                                                kCFURLPOSIXPathStyle,
49                                                true);
50   CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
51   CFRelease (url);
52   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
53   
54   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
55   NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path);
56
57 //  NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr);
58   return (struct xscreensaver_function_table *) addr;
59 }
60
61
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.
64 //
65 - (void) setShellPath
66 {
67   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
68   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
69   
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=");
77   strcat (npath, dir);
78   strcat (npath, ":");
79   strcat (npath, opath);
80   if (putenv (npath)) {
81     perror ("putenv");
82     abort();
83   }
84 //  free (npath);   // Oops, don't free this! putenv() does not copy it!
85 }
86
87
88 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
89 // (e.g., "xscreensaver-text") know how to look up resources.
90 //
91 - (void) setResourcesEnv:(NSString *) name
92 {
93   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
94   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
95   
96   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
97   char *env = (char *) malloc (strlen (s) + 40);
98   strcpy (env, "XSCREENSAVER_CLASSPATH=");
99   strcat (env, s);
100   if (putenv (env)) {
101     perror ("putenv");
102     abort();
103   }
104   free (env);
105 }
106
107
108 static void
109 add_default_options (const XrmOptionDescRec *opts,
110                      const char * const *defs,
111                      XrmOptionDescRec **opts_ret,
112                      const char ***defs_ret)
113 {
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.
118   */
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 },
129     { "-fps",                    ".doFPS",             XrmoptionNoArg, "True" },
130     { "-no-fps",                 ".doFPS",             XrmoptionNoArg, "False"},
131     { 0, 0, 0, 0 }
132   };
133   static const char *default_defaults [] = {
134     ".doFPS:              False",
135     ".textMode:           date",
136  // ".textLiteral:        ",
137  // ".textFile:           ",
138  // ".textURL:            ",
139     ".grabDesktopImages:  yes",
140     ".chooseRandomImages: no",
141     ".imageDirectory:     ~/Pictures",
142     0
143   };
144
145   int count = 0, i, j;
146   for (i = 0; default_options[i].option; i++)
147     count++;
148   for (i = 0; opts[i].option; i++)
149     count++;
150
151   XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
152     calloc (count + 1, sizeof (*opts2));
153
154   i = 0;
155   j = 0;
156   while (default_options[j].option) {
157     opts2[i] = default_options[j];
158     i++, j++;
159   }
160   j = 0;
161   while (opts[j].option) {
162     opts2[i] = opts[j];
163     i++, j++;
164   }
165
166   *opts_ret = opts2;
167
168
169   /* now the defaults
170    */
171   count = 0;
172   for (i = 0; default_defaults[i]; i++)
173     count++;
174   for (i = 0; defs[i]; i++)
175     count++;
176
177   const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
178
179   i = 0;
180   j = 0;
181   while (default_defaults[j]) {
182     defs2[i] = default_defaults[j];
183     i++, j++;
184   }
185   j = 0;
186   while (defs[j]) {
187     defs2[i] = defs[j];
188     i++, j++;
189   }
190
191   *defs_ret = defs2;
192 }
193
194
195 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
196 {
197   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
198     return 0;
199   
200   xsft = [self findFunctionTable];
201   [self setShellPath];
202
203   setup_p = YES;
204   if (xsft->setup_cb)
205     xsft->setup_cb (xsft, xsft->setup_arg);
206
207   /* The plist files for these preferences show up in
208      $HOME/Library/Preferences/ByHost/ in a file named like
209      "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
210    */
211   NSString *name = [NSString stringWithCString:xsft->progclass
212                                       encoding:NSUTF8StringEncoding];
213   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
214   [self setResourcesEnv:name];
215
216   
217   XrmOptionDescRec *opts = 0;
218   const char **defs = 0;
219   add_default_options (xsft->options, xsft->defaults, &opts, &defs);
220   prefsReader = [[PrefsReader alloc]
221                   initWithName:name xrmKeys:opts defaults:defs];
222   free (defs);
223   // free (opts);  // bah, we need these! #### leak!
224   xsft->options = opts;
225   
226   progname = progclass = xsft->progclass;
227
228   next_frame_time = 0;
229   
230   return self;
231 }
232
233 - (void) dealloc
234 {
235   NSAssert(![self isAnimating], @"still animating");
236   NSAssert(!xdata, @"xdata not yet freed");
237   if (xdpy)
238     jwxyz_free_display (xdpy);
239   [prefsReader release];
240   [super dealloc];
241 }
242
243 - (PrefsReader *) prefsReader
244 {
245   return prefsReader;
246 }
247
248
249 - (void) startAnimation
250 {
251   NSAssert(![self isAnimating], @"already animating");
252   NSAssert(!initted_p && !xdata, @"already initialized");
253   [super startAnimation];
254   /* We can't draw on the window from this method, so we actually do the
255      initialization of the screen saver (xsft->init_cb) in the first call
256      to animateOneFrame() instead.
257    */
258 }
259
260 - (void)stopAnimation
261 {
262   NSAssert([self isAnimating], @"not animating");
263
264   if (initted_p) {
265
266     [self lockFocus];       // in case something tries to draw from here
267     [self prepareContext];
268
269     /* I considered just not even calling the free callback at all...
270        But webcollage-cocoa needs it, to kill the inferior webcollage
271        processes (since the screen saver framework never generates a
272        SIGPIPE for them...)  Instead, I turned off the free call in
273        xlockmore.c, which is where all of the bogus calls are anyway.
274      */
275     xsft->free_cb (xdpy, xwindow, xdata);
276     [self unlockFocus];
277
278 //  setup_p = NO; // #### wait, do we need this?
279     initted_p = NO;
280     xdata = 0;
281   }
282
283   [super stopAnimation];
284 }
285
286
287 /* Hook for the XScreenSaverGLView subclass
288  */
289 - (void) prepareContext
290 {
291 }
292
293 /* Hook for the XScreenSaverGLView subclass
294  */
295 - (void) resizeContext
296 {
297 }
298
299
300 static void
301 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
302 {
303   fps_compute (fpst, 0);
304   fps_draw (fpst);
305 }
306
307
308 - (void) animateOneFrame
309 {
310   if (!initted_p) {
311
312     if (! xdpy) {
313       xdpy = jwxyz_make_display (self);
314       xwindow = XRootWindow (xdpy, 0);
315     }
316
317     if (!setup_p) {
318       setup_p = YES;
319       if (xsft->setup_cb)
320         xsft->setup_cb (xsft, xsft->setup_arg);
321     }
322     initted_p = YES;
323     resized_p = NO;
324     NSAssert(!xdata, @"xdata already initialized");
325     
326 # undef ya_rand_init
327     ya_rand_init (0);
328     
329     XSetWindowBackground (xdpy, xwindow,
330                           get_pixel_resource (xdpy, 0,
331                                               "background", "Background"));
332     XClearWindow (xdpy, xwindow);
333     
334     [[self window] setAcceptsMouseMovedEvents:YES];
335
336     /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
337        drawing primitives will run on the GPU instead of the CPU.
338        It seems like it might make things worse rather than better,
339        though...  Plus it makes us binary-incompatible with 10.4.
340
341 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
342     [[self window] setPreferredBackingLocation:
343                      NSWindowBackingLocationVideoMemory];
344 # endif
345      */
346
347     /* Kludge: even though the init_cb functions are declared to take 2 args,
348       actually call them with 3, for the benefit of xlockmore_init() and
349       xlockmore_setup().
350       */
351     void *(*init_cb) (Display *, Window, void *) = 
352       (void *(*) (Display *, Window, void *)) xsft->init_cb;
353     
354     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
355
356     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
357       fpst = fps_init (xdpy, xwindow);
358       if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
359     }
360   }
361
362   /* I don't understand why we have to do this *every frame*, but we do,
363      or else the cursor comes back on.
364    */
365   if (![self isPreview])
366     [NSCursor setHiddenUntilMouseMoves:YES];
367
368
369   if (fpst)
370     {
371       /* This is just a guess, but the -fps code wants to know how long
372          we were sleeping between frames.
373        */
374       unsigned long usecs = 1000000 * [self animationTimeInterval];
375       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
376       fps_slept (fpst, usecs);
377     }
378
379
380   /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
381      This is bad, because some of the screen hacks want to delay for long 
382      periods (like 5 seconds or a minute!) between frames, and running them
383      all at 60 FPS is no good.
384   
385      So, we don't use setAnimationTimeInterval, and just let the framework call
386      us whenever.  But, we only invoke the screen hack's "draw frame" method
387      when enough time has expired.
388   
389      This means two extra calls to gettimeofday() per frame.  For fast-cycling
390      screen savers, that might actually slow them down.  Oh well.
391
392      #### Also, we do not run the draw callback faster than the system's
393           animationTimeInterval, so if any savers are pickier about timing
394           than that, this may slow them down too much.  If that's a problem,
395           then we could call draw_cb in a loop here (with usleep) until the
396           next call would put us past animationTimeInterval...  But a better
397           approach would probably be to just change the saver to not do that.
398    */
399   struct timeval tv;
400   gettimeofday (&tv, 0);
401   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
402   if (now < next_frame_time) return;
403   
404   [self prepareContext];
405
406   if (resized_p) {
407     // We do this here instead of in setFrameSize so that all the
408     // Xlib drawing takes place under the animation timer.
409     [self resizeContext];
410     NSRect r = [self frame];
411     xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
412     resized_p = NO;
413   }
414
415   // Run any XtAppAddInput callbacks now.
416   // (Note that XtAppAddTimeOut callbacks have already been run by
417   // the Cocoa event loop.)
418   //
419   jwxyz_sources_run (display_sources_data (xdpy));
420
421   // And finally:
422   //
423   NSDisableScreenUpdates();
424   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
425   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
426   XSync (xdpy, 0);
427   NSEnableScreenUpdates();
428
429   gettimeofday (&tv, 0);
430   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
431   next_frame_time = now + (delay / 1000000.0);
432 }
433
434
435 - (void)drawRect:(NSRect)rect
436 {
437   if (xwindow)    // clear to the X window's bg color, not necessarily black.
438     XClearWindow (xdpy, xwindow);
439   else
440     [super drawRect:rect];    // early: black.
441 }
442
443
444 - (void) setFrameSize:(NSSize) newSize
445 {
446   [super setFrameSize:newSize];
447   if ([self isAnimating]) {
448     resized_p = YES;
449   }
450 }
451
452 - (void) setFrame:(NSRect) newRect
453 {
454   [super setFrame:newRect];
455   if (xwindow)   // inform Xlib that the window has changed.
456     jwxyz_window_resized (xdpy, xwindow);
457 }
458
459
460 +(BOOL) performGammaFade
461 {
462   return YES;
463 }
464
465 - (BOOL) hasConfigureSheet
466 {
467   return YES;
468 }
469
470 - (NSWindow *) configureSheet
471 {
472   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
473   NSString *file = [NSString stringWithCString:xsft->progclass
474                                       encoding:NSUTF8StringEncoding];
475   file = [file lowercaseString];
476   NSString *path = [bundle pathForResource:file ofType:@"xml"];
477   if (!path) {
478     NSLog (@"%@.xml does not exist in the application bundle: %@/",
479            file, [bundle resourcePath]);
480     return nil;
481   }
482   
483   NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
484                      initWithXMLFile:path
485                              options:xsft->options
486                           controller:[prefsReader userDefaultsController]];
487   
488   // #### am I expected to retain this, or not? wtf.
489   //      I thought not, but if I don't do this, we (sometimes) crash.
490   [sheet retain];
491
492   return sheet;
493 }
494
495
496 /* Announce our willingness to accept keyboard input.
497 */
498 - (BOOL)acceptsFirstResponder
499 {
500   return YES;
501 }
502
503
504 /* Convert an NSEvent into an XEvent, and pass it along.
505    Returns YES if it was handled.
506  */
507 - (BOOL) doEvent: (NSEvent *) e
508             type: (int) type
509 {
510   if (![self isPreview] ||     // no event handling if actually screen-saving!
511       ![self isAnimating] ||
512       !initted_p)
513     return NO;
514   
515   XEvent xe;
516   memset (&xe, 0, sizeof(xe));
517   
518   int state = 0;
519   
520   int flags = [e modifierFlags];
521   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
522   if (flags & NSShiftKeyMask)      state |= ShiftMask;
523   if (flags & NSControlKeyMask)    state |= ControlMask;
524   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
525   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
526   
527   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
528                                             toView:self];
529   int x = p.x;
530   int y = [self frame].size.height - p.y;
531   
532   xe.xany.type = type;
533   switch (type) {
534     case ButtonPress:
535     case ButtonRelease:
536       xe.xbutton.x = x;
537       xe.xbutton.y = y;
538       xe.xbutton.state = state;
539       if ([e type] == NSScrollWheel)
540         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
541                              [e deltaY] < 0 ? Button5 :
542                              [e deltaX] > 0 ? Button6 :
543                              [e deltaX] < 0 ? Button7 :
544                              0);
545       else
546         xe.xbutton.button = [e buttonNumber] + 1;
547       break;
548     case MotionNotify:
549       xe.xmotion.x = x;
550       xe.xmotion.y = y;
551       xe.xmotion.state = state;
552       break;
553     case KeyPress:
554     case KeyRelease:
555       {
556         NSString *nss = [e characters];
557         const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding];
558         xe.xkey.keycode = (s && *s ? *s : 0);
559         xe.xkey.state = state;
560         break;
561       }
562     default:
563       abort();
564   }
565
566   [self lockFocus];
567   [self prepareContext];
568   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
569   [self unlockFocus];
570   return result;
571 }
572
573
574 - (void) mouseDown: (NSEvent *) e
575 {
576   if (! [self doEvent:e type:ButtonPress])
577     [super mouseDown:e];
578 }
579
580 - (void) mouseUp: (NSEvent *) e
581 {
582   if (! [self doEvent:e type:ButtonRelease])
583     [super mouseUp:e];
584 }
585
586 - (void) otherMouseDown: (NSEvent *) e
587 {
588   if (! [self doEvent:e type:ButtonPress])
589     [super otherMouseDown:e];
590 }
591
592 - (void) otherMouseUp: (NSEvent *) e
593 {
594   if (! [self doEvent:e type:ButtonRelease])
595     [super otherMouseUp:e];
596 }
597
598 - (void) mouseMoved: (NSEvent *) e
599 {
600   if (! [self doEvent:e type:MotionNotify])
601     [super mouseMoved:e];
602 }
603
604 - (void) mouseDragged: (NSEvent *) e
605 {
606   if (! [self doEvent:e type:MotionNotify])
607     [super mouseDragged:e];
608 }
609
610 - (void) otherMouseDragged: (NSEvent *) e
611 {
612   if (! [self doEvent:e type:MotionNotify])
613     [super otherMouseDragged:e];
614 }
615
616 - (void) scrollWheel: (NSEvent *) e
617 {
618   if (! [self doEvent:e type:ButtonPress])
619     [super scrollWheel:e];
620 }
621
622 - (void) keyDown: (NSEvent *) e
623 {
624   if (! [self doEvent:e type:KeyPress])
625     [super keyDown:e];
626 }
627
628 - (void) keyUp: (NSEvent *) e
629 {
630   if (! [self doEvent:e type:KeyRelease])
631     [super keyUp:e];
632 }
633
634
635 @end
636
637 /* Utility functions...
638  */
639
640 static PrefsReader *
641 get_prefsReader (Display *dpy)
642 {
643   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
644   if (!view) abort();
645   return [view prefsReader];
646 }
647
648
649 char *
650 get_string_resource (Display *dpy, char *name, char *class)
651 {
652   return [get_prefsReader(dpy) getStringResource:name];
653 }
654
655 Bool
656 get_boolean_resource (Display *dpy, char *name, char *class)
657 {
658   return [get_prefsReader(dpy) getBooleanResource:name];
659 }
660
661 int
662 get_integer_resource (Display *dpy, char *name, char *class)
663 {
664   return [get_prefsReader(dpy) getIntegerResource:name];
665 }
666
667 double
668 get_float_resource (Display *dpy, char *name, char *class)
669 {
670   return [get_prefsReader(dpy) getFloatResource:name];
671 }