http://www.jwz.org/xscreensaver/xscreensaver-5.13.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
1 /* xscreensaver, Copyright (c) 2006-2011 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 /* 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.
26  */
27 #ifndef  MAC_OS_X_VERSION_10_6
28 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
29 #endif
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
33 #endif
34
35 extern struct xscreensaver_function_table *xscreensaver_function_table;
36
37 /* Global variables used by the screen savers
38  */
39 const char *progname;
40 const char *progclass;
41 int mono_p = 0;
42
43
44 @implementation XScreenSaverView
45
46 - (struct xscreensaver_function_table *) findFunctionTable
47 {
48   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
49   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
50   
51   NSString *path = [nsb bundlePath];
52   NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension]
53                     lowercaseString];
54   NSString *suffix = @"_xscreensaver_function_table";
55   NSString *table_name = [name stringByAppendingString:suffix];
56   
57   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
58                                                (CFStringRef) path,
59                                                kCFURLPOSIXPathStyle,
60                                                true);
61   CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
62   CFRelease (url);
63   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
64   
65   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
66   NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path);
67   CFRelease (cfb);
68
69 //  NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr);
70   return (struct xscreensaver_function_table *) addr;
71 }
72
73
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.
76 //
77 - (void) setShellPath
78 {
79   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
80   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
81   
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=");
89   strcat (npath, dir);
90   strcat (npath, ":");
91   strcat (npath, opath);
92   if (putenv (npath)) {
93     perror ("putenv");
94     abort();
95   }
96
97   /* Don't free (npath) -- MacOS's putenv() does not copy it. */
98 }
99
100
101 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
102 // (e.g., "xscreensaver-text") know how to look up resources.
103 //
104 - (void) setResourcesEnv:(NSString *) name
105 {
106   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
107   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
108   
109   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
110   char *env = (char *) malloc (strlen (s) + 40);
111   strcpy (env, "XSCREENSAVER_CLASSPATH=");
112   strcat (env, s);
113   if (putenv (env)) {
114     perror ("putenv");
115     abort();
116   }
117   /* Don't free (env) -- MacOS's putenv() does not copy it. */
118 }
119
120
121 static void
122 add_default_options (const XrmOptionDescRec *opts,
123                      const char * const *defs,
124                      XrmOptionDescRec **opts_ret,
125                      const char ***defs_ret)
126 {
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.
131   */
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"},
144     { 0, 0, 0, 0 }
145   };
146   static const char *default_defaults [] = {
147     ".doFPS:              False",
148     ".doubleBuffer:       True",
149     ".multiSample:        False",
150     ".textMode:           date",
151  // ".textLiteral:        ",
152  // ".textFile:           ",
153  // ".textURL:            ",
154     ".grabDesktopImages:  yes",
155     ".chooseRandomImages: no",
156     ".imageDirectory:     ~/Pictures",
157     0
158   };
159
160   int count = 0, i, j;
161   for (i = 0; default_options[i].option; i++)
162     count++;
163   for (i = 0; opts[i].option; i++)
164     count++;
165
166   XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
167     calloc (count + 1, sizeof (*opts2));
168
169   i = 0;
170   j = 0;
171   while (default_options[j].option) {
172     opts2[i] = default_options[j];
173     i++, j++;
174   }
175   j = 0;
176   while (opts[j].option) {
177     opts2[i] = opts[j];
178     i++, j++;
179   }
180
181   *opts_ret = opts2;
182
183
184   /* now the defaults
185    */
186   count = 0;
187   for (i = 0; default_defaults[i]; i++)
188     count++;
189   for (i = 0; defs[i]; i++)
190     count++;
191
192   const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
193
194   i = 0;
195   j = 0;
196   while (default_defaults[j]) {
197     defs2[i] = default_defaults[j];
198     i++, j++;
199   }
200   j = 0;
201   while (defs[j]) {
202     defs2[i] = defs[j];
203     i++, j++;
204   }
205
206   *defs_ret = defs2;
207 }
208
209
210 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
211 {
212   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
213     return 0;
214   
215   xsft = [self findFunctionTable];
216   [self setShellPath];
217
218   setup_p = YES;
219   if (xsft->setup_cb)
220     xsft->setup_cb (xsft, xsft->setup_arg);
221
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"
225    */
226   NSString *name = [NSString stringWithCString:xsft->progclass
227                                       encoding:NSUTF8StringEncoding];
228   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
229   [self setResourcesEnv:name];
230
231   
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];
237   free (defs);
238   // free (opts);  // bah, we need these! #### leak!
239   xsft->options = opts;
240   
241   progname = progclass = xsft->progclass;
242
243   next_frame_time = 0;
244   
245   return self;
246 }
247
248 - (void) dealloc
249 {
250   NSAssert(![self isAnimating], @"still animating");
251   NSAssert(!xdata, @"xdata not yet freed");
252   if (xdpy)
253     jwxyz_free_display (xdpy);
254   [prefsReader release];
255   [super dealloc];
256 }
257
258 - (PrefsReader *) prefsReader
259 {
260   return prefsReader;
261 }
262
263
264 - (void) startAnimation
265 {
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.
272    */
273 }
274
275 - (void)stopAnimation
276 {
277   NSAssert([self isAnimating], @"not animating");
278
279   if (initted_p) {
280
281     [self lockFocus];       // in case something tries to draw from here
282     [self prepareContext];
283
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.
289      */
290     xsft->free_cb (xdpy, xwindow, xdata);
291     [self unlockFocus];
292
293 //  setup_p = NO; // #### wait, do we need this?
294     initted_p = NO;
295     xdata = 0;
296   }
297
298   [super stopAnimation];
299 }
300
301
302 /* Hook for the XScreenSaverGLView subclass
303  */
304 - (void) prepareContext
305 {
306 }
307
308 /* Hook for the XScreenSaverGLView subclass
309  */
310 - (void) resizeContext
311 {
312 }
313
314
315 static void
316 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
317 {
318   fps_compute (fpst, 0);
319   fps_draw (fpst);
320 }
321
322
323 - (void) animateOneFrame
324 {
325   if (!initted_p) {
326
327     if (! xdpy) {
328       xdpy = jwxyz_make_display (self);
329       xwindow = XRootWindow (xdpy, 0);
330     }
331
332     if (!setup_p) {
333       setup_p = YES;
334       if (xsft->setup_cb)
335         xsft->setup_cb (xsft, xsft->setup_arg);
336     }
337     initted_p = YES;
338     resized_p = NO;
339     NSAssert(!xdata, @"xdata already initialized");
340     
341 # undef ya_rand_init
342     ya_rand_init (0);
343     
344     XSetWindowBackground (xdpy, xwindow,
345                           get_pixel_resource (xdpy, 0,
346                                               "background", "Background"));
347     XClearWindow (xdpy, xwindow);
348     
349     [[self window] setAcceptsMouseMovedEvents:YES];
350
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.
355
356 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
357     [[self window] setPreferredBackingLocation:
358                      NSWindowBackingLocationVideoMemory];
359 # endif
360      */
361
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
364       xlockmore_setup().
365       */
366     void *(*init_cb) (Display *, Window, void *) = 
367       (void *(*) (Display *, Window, void *)) xsft->init_cb;
368     
369     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
370
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;
374     }
375   }
376
377   /* I don't understand why we have to do this *every frame*, but we do,
378      or else the cursor comes back on.
379    */
380   if (![self isPreview])
381     [NSCursor setHiddenUntilMouseMoves:YES];
382
383
384   if (fpst)
385     {
386       /* This is just a guess, but the -fps code wants to know how long
387          we were sleeping between frames.
388        */
389       unsigned long usecs = 1000000 * [self animationTimeInterval];
390       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
391       fps_slept (fpst, usecs);
392     }
393
394
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.
399   
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.
403   
404      This means two extra calls to gettimeofday() per frame.  For fast-cycling
405      screen savers, that might actually slow them down.  Oh well.
406
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.
413    */
414   struct timeval tv;
415   gettimeofday (&tv, 0);
416   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
417   if (now < next_frame_time) return;
418   
419   [self prepareContext];
420
421   if (resized_p) {
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);
427     resized_p = NO;
428   }
429
430   // Run any XtAppAddInput callbacks now.
431   // (Note that XtAppAddTimeOut callbacks have already been run by
432   // the Cocoa event loop.)
433   //
434   jwxyz_sources_run (display_sources_data (xdpy));
435
436   // And finally:
437   //
438   NSDisableScreenUpdates();
439   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
440   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
441   XSync (xdpy, 0);
442   NSEnableScreenUpdates();
443
444   gettimeofday (&tv, 0);
445   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
446   next_frame_time = now + (delay / 1000000.0);
447
448
449 # ifdef DO_GC_HACKERY
450   /* Current theory is that the 10.6 garbage collector sucks in the
451      following way:
452
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
464      sodomized.
465
466      So, we force an exhaustive garbage collection in this process
467      approximately every 5 seconds whether the system thinks it needs 
468      one or not.
469   */
470   {
471     static int tick = 0;
472     if (++tick > 5*30) {
473       tick = 0;
474       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
475     }
476   }
477 # endif // DO_GC_HACKERY
478 }
479
480
481 - (void)drawRect:(NSRect)rect
482 {
483   if (xwindow)    // clear to the X window's bg color, not necessarily black.
484     XClearWindow (xdpy, xwindow);
485   else
486     [super drawRect:rect];    // early: black.
487 }
488
489
490 - (void) setFrameSize:(NSSize) newSize
491 {
492   [super setFrameSize:newSize];
493   if ([self isAnimating]) {
494     resized_p = YES;
495   }
496 }
497
498 - (void) setFrame:(NSRect) newRect
499 {
500   [super setFrame:newRect];
501   if (xwindow)   // inform Xlib that the window has changed.
502     jwxyz_window_resized (xdpy, xwindow);
503 }
504
505
506 +(BOOL) performGammaFade
507 {
508   return YES;
509 }
510
511 - (BOOL) hasConfigureSheet
512 {
513   return YES;
514 }
515
516 - (NSWindow *) configureSheet
517 {
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"];
523   if (!path) {
524     NSLog (@"%@.xml does not exist in the application bundle: %@/",
525            file, [bundle resourcePath]);
526     return nil;
527   }
528   
529   NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
530                      initWithXMLFile:path
531                              options:xsft->options
532                           controller:[prefsReader userDefaultsController]];
533   
534   // #### am I expected to retain this, or not? wtf.
535   //      I thought not, but if I don't do this, we (sometimes) crash.
536   [sheet retain];
537
538   return sheet;
539 }
540
541
542 /* Announce our willingness to accept keyboard input.
543 */
544 - (BOOL)acceptsFirstResponder
545 {
546   return YES;
547 }
548
549
550 /* Convert an NSEvent into an XEvent, and pass it along.
551    Returns YES if it was handled.
552  */
553 - (BOOL) doEvent: (NSEvent *) e
554             type: (int) type
555 {
556   if (![self isPreview] ||     // no event handling if actually screen-saving!
557       ![self isAnimating] ||
558       !initted_p)
559     return NO;
560   
561   XEvent xe;
562   memset (&xe, 0, sizeof(xe));
563   
564   int state = 0;
565   
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;
572   
573   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
574                                             toView:self];
575   int x = p.x;
576   int y = [self frame].size.height - p.y;
577   
578   xe.xany.type = type;
579   switch (type) {
580     case ButtonPress:
581     case ButtonRelease:
582       xe.xbutton.x = x;
583       xe.xbutton.y = 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 :
590                              0);
591       else
592         xe.xbutton.button = [e buttonNumber] + 1;
593       break;
594     case MotionNotify:
595       xe.xmotion.x = x;
596       xe.xmotion.y = y;
597       xe.xmotion.state = state;
598       break;
599     case KeyPress:
600     case KeyRelease:
601       {
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;
606         break;
607       }
608     default:
609       abort();
610   }
611
612   [self lockFocus];
613   [self prepareContext];
614   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
615   [self unlockFocus];
616   return result;
617 }
618
619
620 - (void) mouseDown: (NSEvent *) e
621 {
622   if (! [self doEvent:e type:ButtonPress])
623     [super mouseDown:e];
624 }
625
626 - (void) mouseUp: (NSEvent *) e
627 {
628   if (! [self doEvent:e type:ButtonRelease])
629     [super mouseUp:e];
630 }
631
632 - (void) otherMouseDown: (NSEvent *) e
633 {
634   if (! [self doEvent:e type:ButtonPress])
635     [super otherMouseDown:e];
636 }
637
638 - (void) otherMouseUp: (NSEvent *) e
639 {
640   if (! [self doEvent:e type:ButtonRelease])
641     [super otherMouseUp:e];
642 }
643
644 - (void) mouseMoved: (NSEvent *) e
645 {
646   if (! [self doEvent:e type:MotionNotify])
647     [super mouseMoved:e];
648 }
649
650 - (void) mouseDragged: (NSEvent *) e
651 {
652   if (! [self doEvent:e type:MotionNotify])
653     [super mouseDragged:e];
654 }
655
656 - (void) otherMouseDragged: (NSEvent *) e
657 {
658   if (! [self doEvent:e type:MotionNotify])
659     [super otherMouseDragged:e];
660 }
661
662 - (void) scrollWheel: (NSEvent *) e
663 {
664   if (! [self doEvent:e type:ButtonPress])
665     [super scrollWheel:e];
666 }
667
668 - (void) keyDown: (NSEvent *) e
669 {
670   if (! [self doEvent:e type:KeyPress])
671     [super keyDown:e];
672 }
673
674 - (void) keyUp: (NSEvent *) e
675 {
676   if (! [self doEvent:e type:KeyRelease])
677     [super keyUp:e];
678 }
679
680
681 @end
682
683 /* Utility functions...
684  */
685
686 static PrefsReader *
687 get_prefsReader (Display *dpy)
688 {
689   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
690   if (!view) abort();
691   return [view prefsReader];
692 }
693
694
695 char *
696 get_string_resource (Display *dpy, char *name, char *class)
697 {
698   return [get_prefsReader(dpy) getStringResource:name];
699 }
700
701 Bool
702 get_boolean_resource (Display *dpy, char *name, char *class)
703 {
704   return [get_prefsReader(dpy) getBooleanResource:name];
705 }
706
707 int
708 get_integer_resource (Display *dpy, char *name, char *class)
709 {
710   return [get_prefsReader(dpy) getIntegerResource:name];
711 }
712
713 double
714 get_float_resource (Display *dpy, char *name, char *class)
715 {
716   return [get_prefsReader(dpy) getFloatResource:name];
717 }