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