From http://www.jwz.org/xscreensaver/xscreensaver-5.15.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     { "-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"},
145     { 0, 0, 0, 0 }
146   };
147   static const char *default_defaults [] = {
148     ".doFPS:              False",
149     ".doubleBuffer:       True",
150     ".multiSample:        False",
151     ".textMode:           date",
152  // ".textLiteral:        ",
153  // ".textFile:           ",
154     ".textURL:            http://twitter.com/statuses/public_timeline.atom",
155  // ".textProgram:        ",
156     ".grabDesktopImages:  yes",
157     ".chooseRandomImages: no",
158     ".imageDirectory:     ~/Pictures",
159     0
160   };
161
162   int count = 0, i, j;
163   for (i = 0; default_options[i].option; i++)
164     count++;
165   for (i = 0; opts[i].option; i++)
166     count++;
167
168   XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
169     calloc (count + 1, sizeof (*opts2));
170
171   i = 0;
172   j = 0;
173   while (default_options[j].option) {
174     opts2[i] = default_options[j];
175     i++, j++;
176   }
177   j = 0;
178   while (opts[j].option) {
179     opts2[i] = opts[j];
180     i++, j++;
181   }
182
183   *opts_ret = opts2;
184
185
186   /* now the defaults
187    */
188   count = 0;
189   for (i = 0; default_defaults[i]; i++)
190     count++;
191   for (i = 0; defs[i]; i++)
192     count++;
193
194   const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
195
196   i = 0;
197   j = 0;
198   while (default_defaults[j]) {
199     defs2[i] = default_defaults[j];
200     i++, j++;
201   }
202   j = 0;
203   while (defs[j]) {
204     defs2[i] = defs[j];
205     i++, j++;
206   }
207
208   *defs_ret = defs2;
209 }
210
211
212 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
213 {
214   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
215     return 0;
216   
217   xsft = [self findFunctionTable];
218   [self setShellPath];
219
220   setup_p = YES;
221   if (xsft->setup_cb)
222     xsft->setup_cb (xsft, xsft->setup_arg);
223
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"
227    */
228   NSString *name = [NSString stringWithCString:xsft->progclass
229                                       encoding:NSUTF8StringEncoding];
230   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
231   [self setResourcesEnv:name];
232
233   
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];
239   free (defs);
240   // free (opts);  // bah, we need these! #### leak!
241   xsft->options = opts;
242   
243   progname = progclass = xsft->progclass;
244
245   next_frame_time = 0;
246   
247   return self;
248 }
249
250 - (void) dealloc
251 {
252   NSAssert(![self isAnimating], @"still animating");
253   NSAssert(!xdata, @"xdata not yet freed");
254   if (xdpy)
255     jwxyz_free_display (xdpy);
256   [prefsReader release];
257   [super dealloc];
258 }
259
260 - (PrefsReader *) prefsReader
261 {
262   return prefsReader;
263 }
264
265
266 - (void) startAnimation
267 {
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.
274    */
275 }
276
277 - (void)stopAnimation
278 {
279   NSAssert([self isAnimating], @"not animating");
280
281   if (initted_p) {
282
283     [self lockFocus];       // in case something tries to draw from here
284     [self prepareContext];
285
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.
291      */
292     xsft->free_cb (xdpy, xwindow, xdata);
293     [self unlockFocus];
294
295 //  setup_p = NO; // #### wait, do we need this?
296     initted_p = NO;
297     xdata = 0;
298   }
299
300   [super stopAnimation];
301 }
302
303
304 /* Hook for the XScreenSaverGLView subclass
305  */
306 - (void) prepareContext
307 {
308 }
309
310 /* Hook for the XScreenSaverGLView subclass
311  */
312 - (void) resizeContext
313 {
314 }
315
316
317 static void
318 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
319 {
320   fps_compute (fpst, 0, -1);
321   fps_draw (fpst);
322 }
323
324
325 - (void) animateOneFrame
326 {
327   if (!initted_p) {
328
329     if (! xdpy) {
330       xdpy = jwxyz_make_display (self);
331       xwindow = XRootWindow (xdpy, 0);
332     }
333
334     if (!setup_p) {
335       setup_p = YES;
336       if (xsft->setup_cb)
337         xsft->setup_cb (xsft, xsft->setup_arg);
338     }
339     initted_p = YES;
340     resized_p = NO;
341     NSAssert(!xdata, @"xdata already initialized");
342     
343 # undef ya_rand_init
344     ya_rand_init (0);
345     
346     XSetWindowBackground (xdpy, xwindow,
347                           get_pixel_resource (xdpy, 0,
348                                               "background", "Background"));
349     XClearWindow (xdpy, xwindow);
350     
351     [[self window] setAcceptsMouseMovedEvents:YES];
352
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.
357
358 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
359     [[self window] setPreferredBackingLocation:
360                      NSWindowBackingLocationVideoMemory];
361 # endif
362      */
363
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
366       xlockmore_setup().
367       */
368     void *(*init_cb) (Display *, Window, void *) = 
369       (void *(*) (Display *, Window, void *)) xsft->init_cb;
370     
371     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
372
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;
376     }
377   }
378
379   /* I don't understand why we have to do this *every frame*, but we do,
380      or else the cursor comes back on.
381    */
382   if (![self isPreview])
383     [NSCursor setHiddenUntilMouseMoves:YES];
384
385
386   if (fpst)
387     {
388       /* This is just a guess, but the -fps code wants to know how long
389          we were sleeping between frames.
390        */
391       unsigned long usecs = 1000000 * [self animationTimeInterval];
392       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
393       fps_slept (fpst, usecs);
394     }
395
396
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.
401   
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.
405   
406      This means two extra calls to gettimeofday() per frame.  For fast-cycling
407      screen savers, that might actually slow them down.  Oh well.
408
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.
415    */
416   struct timeval tv;
417   gettimeofday (&tv, 0);
418   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
419   if (now < next_frame_time) return;
420   
421   [self prepareContext];
422
423   if (resized_p) {
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);
429     resized_p = NO;
430   }
431
432   // Run any XtAppAddInput callbacks now.
433   // (Note that XtAppAddTimeOut callbacks have already been run by
434   // the Cocoa event loop.)
435   //
436   jwxyz_sources_run (display_sources_data (xdpy));
437
438   // And finally:
439   //
440   NSDisableScreenUpdates();
441   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
442   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
443   XSync (xdpy, 0);
444   NSEnableScreenUpdates();
445
446   gettimeofday (&tv, 0);
447   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
448   next_frame_time = now + (delay / 1000000.0);
449
450
451 # ifdef DO_GC_HACKERY
452   /* Current theory is that the 10.6 garbage collector sucks in the
453      following way:
454
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
466      sodomized.
467
468      So, we force an exhaustive garbage collection in this process
469      approximately every 5 seconds whether the system thinks it needs 
470      one or not.
471   */
472   {
473     static int tick = 0;
474     if (++tick > 5*30) {
475       tick = 0;
476       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
477     }
478   }
479 # endif // DO_GC_HACKERY
480 }
481
482
483 - (void)drawRect:(NSRect)rect
484 {
485   if (xwindow)    // clear to the X window's bg color, not necessarily black.
486     XClearWindow (xdpy, xwindow);
487   else
488     [super drawRect:rect];    // early: black.
489 }
490
491
492 - (void) setFrameSize:(NSSize) newSize
493 {
494   [super setFrameSize:newSize];
495   if ([self isAnimating]) {
496     resized_p = YES;
497   }
498 }
499
500 - (void) setFrame:(NSRect) newRect
501 {
502   [super setFrame:newRect];
503   if (xwindow)   // inform Xlib that the window has changed.
504     jwxyz_window_resized (xdpy, xwindow);
505 }
506
507
508 +(BOOL) performGammaFade
509 {
510   return YES;
511 }
512
513 - (BOOL) hasConfigureSheet
514 {
515   return YES;
516 }
517
518 - (NSWindow *) configureSheet
519 {
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"];
525   if (!path) {
526     NSLog (@"%@.xml does not exist in the application bundle: %@/",
527            file, [bundle resourcePath]);
528     return nil;
529   }
530   
531   NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
532                      initWithXMLFile:path
533                              options:xsft->options
534                           controller:[prefsReader userDefaultsController]];
535   
536   // #### am I expected to retain this, or not? wtf.
537   //      I thought not, but if I don't do this, we (sometimes) crash.
538   [sheet retain];
539
540   return sheet;
541 }
542
543
544 - (NSUserDefaultsController *) userDefaultsController
545 {
546   return [prefsReader userDefaultsController];
547 }
548
549
550 /* Announce our willingness to accept keyboard input.
551 */
552 - (BOOL)acceptsFirstResponder
553 {
554   return YES;
555 }
556
557
558 /* Convert an NSEvent into an XEvent, and pass it along.
559    Returns YES if it was handled.
560  */
561 - (BOOL) doEvent: (NSEvent *) e
562             type: (int) type
563 {
564   if (![self isPreview] ||     // no event handling if actually screen-saving!
565       ![self isAnimating] ||
566       !initted_p)
567     return NO;
568   
569   XEvent xe;
570   memset (&xe, 0, sizeof(xe));
571   
572   int state = 0;
573   
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;
580   
581   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
582                                             toView:self];
583   int x = p.x;
584   int y = [self frame].size.height - p.y;
585   
586   xe.xany.type = type;
587   switch (type) {
588     case ButtonPress:
589     case ButtonRelease:
590       xe.xbutton.x = x;
591       xe.xbutton.y = 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 :
598                              0);
599       else
600         xe.xbutton.button = [e buttonNumber] + 1;
601       break;
602     case MotionNotify:
603       xe.xmotion.x = x;
604       xe.xmotion.y = y;
605       xe.xmotion.state = state;
606       break;
607     case KeyPress:
608     case KeyRelease:
609       {
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;
614         break;
615       }
616     default:
617       abort();
618   }
619
620   [self lockFocus];
621   [self prepareContext];
622   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
623   [self unlockFocus];
624   return result;
625 }
626
627
628 - (void) mouseDown: (NSEvent *) e
629 {
630   if (! [self doEvent:e type:ButtonPress])
631     [super mouseDown:e];
632 }
633
634 - (void) mouseUp: (NSEvent *) e
635 {
636   if (! [self doEvent:e type:ButtonRelease])
637     [super mouseUp:e];
638 }
639
640 - (void) otherMouseDown: (NSEvent *) e
641 {
642   if (! [self doEvent:e type:ButtonPress])
643     [super otherMouseDown:e];
644 }
645
646 - (void) otherMouseUp: (NSEvent *) e
647 {
648   if (! [self doEvent:e type:ButtonRelease])
649     [super otherMouseUp:e];
650 }
651
652 - (void) mouseMoved: (NSEvent *) e
653 {
654   if (! [self doEvent:e type:MotionNotify])
655     [super mouseMoved:e];
656 }
657
658 - (void) mouseDragged: (NSEvent *) e
659 {
660   if (! [self doEvent:e type:MotionNotify])
661     [super mouseDragged:e];
662 }
663
664 - (void) otherMouseDragged: (NSEvent *) e
665 {
666   if (! [self doEvent:e type:MotionNotify])
667     [super otherMouseDragged:e];
668 }
669
670 - (void) scrollWheel: (NSEvent *) e
671 {
672   if (! [self doEvent:e type:ButtonPress])
673     [super scrollWheel:e];
674 }
675
676 - (void) keyDown: (NSEvent *) e
677 {
678   if (! [self doEvent:e type:KeyPress])
679     [super keyDown:e];
680 }
681
682 - (void) keyUp: (NSEvent *) e
683 {
684   if (! [self doEvent:e type:KeyRelease])
685     [super keyUp:e];
686 }
687
688
689 @end
690
691 /* Utility functions...
692  */
693
694 static PrefsReader *
695 get_prefsReader (Display *dpy)
696 {
697   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
698   if (!view) abort();
699   return [view prefsReader];
700 }
701
702
703 char *
704 get_string_resource (Display *dpy, char *name, char *class)
705 {
706   return [get_prefsReader(dpy) getStringResource:name];
707 }
708
709 Bool
710 get_boolean_resource (Display *dpy, char *name, char *class)
711 {
712   return [get_prefsReader(dpy) getBooleanResource:name];
713 }
714
715 int
716 get_integer_resource (Display *dpy, char *name, char *class)
717 {
718   return [get_prefsReader(dpy) getIntegerResource:name];
719 }
720
721 double
722 get_float_resource (Display *dpy, char *name, char *class)
723 {
724   return [get_prefsReader(dpy) getFloatResource:name];
725 }