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