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