http://slackware.bholcomb.com/slackware/slackware-11.0/source/xap/xscreensaver/xscree...
[xscreensaver] / OSX / XScreenSaverView.m
1 /* xscreensaver, Copyright (c) 2006 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     /* Kludge: even though the init_cb functions are declared to take 2 args,
318       actually call them with 3, for the benefit of xlockmore_init() and
319       xlockmore_setup().
320       */
321     void *(*init_cb) (Display *, Window, void *) = 
322       (void *(*) (Display *, Window, void *)) xsft->init_cb;
323     
324     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
325   }
326
327   /* I don't understand why we have to do this *every frame*, but we do,
328      or else the cursor comes back on.
329    */
330   if (![self isPreview])
331     [NSCursor setHiddenUntilMouseMoves:YES];
332
333   /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
334      This is bad, because some of the screen hacks want to delay for long 
335      periods (like 5 seconds or a minute!) between frames, and running them
336      all at 60 FPS is no good.
337   
338      So, we don't use setAnimationTimeInterval, and just let the framework call
339      us whenever.  But, we only invoke the screen hack's "draw frame" method
340      when enough time has expired.
341   
342      This means two extra calls to gettimeofday() per frame.  For fast-cycling
343      screen savers, that might actually slow them down.  Oh well.
344
345      #### Also, we do not run the draw callback faster than the system's
346           animationTimeInterval, so if any savers are pickier about timing
347           than that, this may slow them down too much.  If that's a problem,
348           then we could call draw_cb in a loop here (with usleep) until the
349           next call would put us past animationTimeInterval...  But a better
350           approach would probably be to just change the saver to not do that.
351    */
352   struct timeval tv;
353   gettimeofday (&tv, 0);
354   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
355   if (now < next_frame_time) return;
356   
357   [self prepareContext];
358
359   if (resized_p) {
360     // We do this here instead of in setFrameSize so that all the
361     // Xlib drawing takes place under the animation timer.
362     [self resizeContext];
363     NSRect r = [self frame];
364     xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
365     resized_p = NO;
366   }
367
368   // Run any XtAppAddInput callbacks now.
369   // (Note that XtAppAddTimeOut callbacks have already been run by
370   // the Cocoa event loop.)
371   //
372   jwxyz_sources_run (display_sources_data (xdpy));
373
374   // And finally:
375   //
376   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
377   
378   XSync (xdpy, 0);
379   
380   gettimeofday (&tv, 0);
381   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
382   next_frame_time = now + (delay / 1000000.0);
383 }
384
385
386 - (void)drawRect:(NSRect)rect
387 {
388   if (xwindow)    // clear to the X window's bg color, not necessarily black.
389     XClearWindow (xdpy, xwindow);
390   else
391     [super drawRect:rect];    // early: black.
392 }
393
394
395 - (void) setFrameSize:(NSSize) newSize
396 {
397   [super setFrameSize:newSize];
398   if ([self isAnimating]) {
399     resized_p = YES;
400   }
401 }
402
403 - (void) setFrame:(NSRect) newRect
404 {
405   [super setFrame:newRect];
406   if (xwindow)   // inform Xlib that the window has changed.
407     jwxyz_window_resized (xdpy, xwindow);
408 }
409
410
411 +(BOOL) performGammaFade
412 {
413   return YES;
414 }
415
416 - (BOOL) hasConfigureSheet
417 {
418   return YES;
419 }
420
421 - (NSWindow *) configureSheet
422 {
423   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
424   NSString *file = [NSString stringWithCString:xsft->progclass
425                                       encoding:NSUTF8StringEncoding];
426   file = [file lowercaseString];
427   NSString *path = [bundle pathForResource:file ofType:@"xml"];
428   if (!path) {
429     NSLog (@"%@.xml does not exist in the application bundle: %@/",
430            file, [bundle resourcePath]);
431     return nil;
432   }
433   
434   NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
435                      initWithXMLFile:path
436                              options:xsft->options
437                           controller:[prefsReader userDefaultsController]];
438   
439   // #### am I expected to retain this, or not? wtf.
440   //      I thought not, but if I don't do this, we (sometimes) crash.
441   [sheet retain];
442
443   return sheet;
444 }
445
446
447 /* Announce our willingness to accept keyboard input.
448 */
449 - (BOOL)acceptsFirstResponder
450 {
451   return YES;
452 }
453
454
455 /* Convert an NSEvent into an XEvent, and pass it along.
456    Returns YES if it was handled.
457  */
458 - (BOOL) doEvent: (NSEvent *) e
459             type: (int) type
460 {
461   if (![self isPreview] ||     // no event handling if actually screen-saving!
462       ![self isAnimating] ||
463       !initted_p)
464     return NO;
465   
466   XEvent xe;
467   memset (&xe, 0, sizeof(xe));
468   
469   int state = 0;
470   
471   int flags = [e modifierFlags];
472   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
473   if (flags & NSShiftKeyMask)      state |= ShiftMask;
474   if (flags & NSControlKeyMask)    state |= ControlMask;
475   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
476   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
477   
478   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
479                                             toView:self];
480   int x = p.x;
481   int y = [self frame].size.height - p.y;
482   
483   xe.xany.type = type;
484   switch (type) {
485     case ButtonPress:
486     case ButtonRelease:
487       xe.xbutton.x = x;
488       xe.xbutton.y = y;
489       xe.xbutton.state = state;
490       if ([e type] == NSScrollWheel)
491         xe.xbutton.button = ([e deltaY] > 0 ? Button4 : Button5);
492       else
493         xe.xbutton.button = [e buttonNumber] + 1;
494       break;
495     case MotionNotify:
496       xe.xmotion.x = x;
497       xe.xmotion.y = y;
498       xe.xmotion.state = state;
499       break;
500     case KeyPress:
501     case KeyRelease:
502       {
503         NSString *nss = [e characters];
504         const char *s = [nss cStringUsingEncoding:NSUTF8StringEncoding];
505         xe.xkey.keycode = (s && *s ? *s : 0);
506         xe.xkey.state = state;
507         break;
508       }
509     default:
510       abort();
511   }
512
513   [self lockFocus];
514   [self prepareContext];
515   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
516   [self unlockFocus];
517   return result;
518 }
519
520
521 - (void) mouseDown: (NSEvent *) e
522 {
523   if (! [self doEvent:e type:ButtonPress])
524     [super mouseDown:e];
525 }
526
527 - (void) mouseUp: (NSEvent *) e
528 {
529   if (! [self doEvent:e type:ButtonRelease])
530     [super mouseUp:e];
531 }
532
533 - (void) otherMouseDown: (NSEvent *) e
534 {
535   if (! [self doEvent:e type:ButtonPress])
536     [super otherMouseDown:e];
537 }
538
539 - (void) otherMouseUp: (NSEvent *) e
540 {
541   if (! [self doEvent:e type:ButtonRelease])
542     [super otherMouseUp:e];
543 }
544
545 - (void) mouseMoved: (NSEvent *) e
546 {
547   if (! [self doEvent:e type:MotionNotify])
548     [super mouseMoved:e];
549 }
550
551 - (void) mouseDragged: (NSEvent *) e
552 {
553   if (! [self doEvent:e type:MotionNotify])
554     [super mouseDragged:e];
555 }
556
557 - (void) otherMouseDragged: (NSEvent *) e
558 {
559   if (! [self doEvent:e type:MotionNotify])
560     [super otherMouseDragged:e];
561 }
562
563 - (void) scrollWheel: (NSEvent *) e
564 {
565   if (! [self doEvent:e type:ButtonPress])
566     [super scrollWheel:e];
567 }
568
569 - (void) keyDown: (NSEvent *) e
570 {
571   if (! [self doEvent:e type:KeyPress])
572     [super keyDown:e];
573 }
574
575 - (void) keyUp: (NSEvent *) e
576 {
577   if (! [self doEvent:e type:KeyRelease])
578     [super keyUp:e];
579 }
580
581
582 @end
583
584 /* Utility functions...
585  */
586
587 static PrefsReader *
588 get_prefsReader (Display *dpy)
589 {
590   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
591   if (!view) abort();
592   return [view prefsReader];
593 }
594
595
596 char *
597 get_string_resource (Display *dpy, char *name, char *class)
598 {
599   return [get_prefsReader(dpy) getStringResource:name];
600 }
601
602 Bool
603 get_boolean_resource (Display *dpy, char *name, char *class)
604 {
605   return [get_prefsReader(dpy) getBooleanResource:name];
606 }
607
608 int
609 get_integer_resource (Display *dpy, char *name, char *class)
610 {
611   return [get_prefsReader(dpy) getIntegerResource:name];
612 }
613
614 double
615 get_float_resource (Display *dpy, char *name, char *class)
616 {
617   return [get_prefsReader(dpy) getFloatResource:name];
618 }