http://www.tienza.es/crux/src/www.jwz.org/xscreensaver/xscreensaver-5.05.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   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
388   
389   XSync (xdpy, 0);
390   
391   gettimeofday (&tv, 0);
392   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
393   next_frame_time = now + (delay / 1000000.0);
394 }
395
396
397 - (void)drawRect:(NSRect)rect
398 {
399   if (xwindow)    // clear to the X window's bg color, not necessarily black.
400     XClearWindow (xdpy, xwindow);
401   else
402     [super drawRect:rect];    // early: black.
403 }
404
405
406 - (void) setFrameSize:(NSSize) newSize
407 {
408   [super setFrameSize:newSize];
409   if ([self isAnimating]) {
410     resized_p = YES;
411   }
412 }
413
414 - (void) setFrame:(NSRect) newRect
415 {
416   [super setFrame:newRect];
417   if (xwindow)   // inform Xlib that the window has changed.
418     jwxyz_window_resized (xdpy, xwindow);
419 }
420
421
422 +(BOOL) performGammaFade
423 {
424   return YES;
425 }
426
427 - (BOOL) hasConfigureSheet
428 {
429   return YES;
430 }
431
432 - (NSWindow *) configureSheet
433 {
434   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
435   NSString *file = [NSString stringWithCString:xsft->progclass
436                                       encoding:NSUTF8StringEncoding];
437   file = [file lowercaseString];
438   NSString *path = [bundle pathForResource:file ofType:@"xml"];
439   if (!path) {
440     NSLog (@"%@.xml does not exist in the application bundle: %@/",
441            file, [bundle resourcePath]);
442     return nil;
443   }
444   
445   NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
446                      initWithXMLFile:path
447                              options:xsft->options
448                           controller:[prefsReader userDefaultsController]];
449   
450   // #### am I expected to retain this, or not? wtf.
451   //      I thought not, but if I don't do this, we (sometimes) crash.
452   [sheet retain];
453
454   return sheet;
455 }
456
457
458 /* Announce our willingness to accept keyboard input.
459 */
460 - (BOOL)acceptsFirstResponder
461 {
462   return YES;
463 }
464
465
466 /* Convert an NSEvent into an XEvent, and pass it along.
467    Returns YES if it was handled.
468  */
469 - (BOOL) doEvent: (NSEvent *) e
470             type: (int) type
471 {
472   if (![self isPreview] ||     // no event handling if actually screen-saving!
473       ![self isAnimating] ||
474       !initted_p)
475     return NO;
476   
477   XEvent xe;
478   memset (&xe, 0, sizeof(xe));
479   
480   int state = 0;
481   
482   int flags = [e modifierFlags];
483   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
484   if (flags & NSShiftKeyMask)      state |= ShiftMask;
485   if (flags & NSControlKeyMask)    state |= ControlMask;
486   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
487   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
488   
489   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
490                                             toView:self];
491   int x = p.x;
492   int y = [self frame].size.height - p.y;
493   
494   xe.xany.type = type;
495   switch (type) {
496     case ButtonPress:
497     case ButtonRelease:
498       xe.xbutton.x = x;
499       xe.xbutton.y = y;
500       xe.xbutton.state = state;
501       if ([e type] == NSScrollWheel)
502         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
503                              [e deltaY] < 0 ? Button5 :
504                              [e deltaX] > 0 ? Button6 :
505                              [e deltaX] < 0 ? Button7 :
506                              0);
507       else
508         xe.xbutton.button = [e buttonNumber] + 1;
509       break;
510     case MotionNotify:
511       xe.xmotion.x = x;
512       xe.xmotion.y = y;
513       xe.xmotion.state = state;
514       break;
515     case KeyPress:
516     case KeyRelease:
517       {
518         NSString *nss = [e characters];
519         const char *s = [nss cStringUsingEncoding:NSUTF8StringEncoding];
520         xe.xkey.keycode = (s && *s ? *s : 0);
521         xe.xkey.state = state;
522         break;
523       }
524     default:
525       abort();
526   }
527
528   [self lockFocus];
529   [self prepareContext];
530   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
531   [self unlockFocus];
532   return result;
533 }
534
535
536 - (void) mouseDown: (NSEvent *) e
537 {
538   if (! [self doEvent:e type:ButtonPress])
539     [super mouseDown:e];
540 }
541
542 - (void) mouseUp: (NSEvent *) e
543 {
544   if (! [self doEvent:e type:ButtonRelease])
545     [super mouseUp:e];
546 }
547
548 - (void) otherMouseDown: (NSEvent *) e
549 {
550   if (! [self doEvent:e type:ButtonPress])
551     [super otherMouseDown:e];
552 }
553
554 - (void) otherMouseUp: (NSEvent *) e
555 {
556   if (! [self doEvent:e type:ButtonRelease])
557     [super otherMouseUp:e];
558 }
559
560 - (void) mouseMoved: (NSEvent *) e
561 {
562   if (! [self doEvent:e type:MotionNotify])
563     [super mouseMoved:e];
564 }
565
566 - (void) mouseDragged: (NSEvent *) e
567 {
568   if (! [self doEvent:e type:MotionNotify])
569     [super mouseDragged:e];
570 }
571
572 - (void) otherMouseDragged: (NSEvent *) e
573 {
574   if (! [self doEvent:e type:MotionNotify])
575     [super otherMouseDragged:e];
576 }
577
578 - (void) scrollWheel: (NSEvent *) e
579 {
580   if (! [self doEvent:e type:ButtonPress])
581     [super scrollWheel:e];
582 }
583
584 - (void) keyDown: (NSEvent *) e
585 {
586   if (! [self doEvent:e type:KeyPress])
587     [super keyDown:e];
588 }
589
590 - (void) keyUp: (NSEvent *) e
591 {
592   if (! [self doEvent:e type:KeyRelease])
593     [super keyUp:e];
594 }
595
596
597 @end
598
599 /* Utility functions...
600  */
601
602 static PrefsReader *
603 get_prefsReader (Display *dpy)
604 {
605   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
606   if (!view) abort();
607   return [view prefsReader];
608 }
609
610
611 char *
612 get_string_resource (Display *dpy, char *name, char *class)
613 {
614   return [get_prefsReader(dpy) getStringResource:name];
615 }
616
617 Bool
618 get_boolean_resource (Display *dpy, char *name, char *class)
619 {
620   return [get_prefsReader(dpy) getBooleanResource:name];
621 }
622
623 int
624 get_integer_resource (Display *dpy, char *name, char *class)
625 {
626   return [get_prefsReader(dpy) getIntegerResource:name];
627 }
628
629 double
630 get_float_resource (Display *dpy, char *name, char *class)
631 {
632   return [get_prefsReader(dpy) getFloatResource:name];
633 }