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