6caf2e8cbf5a45e0beca364f004814de154822e9
[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 : Button5);
503       else
504         xe.xbutton.button = [e buttonNumber] + 1;
505       break;
506     case MotionNotify:
507       xe.xmotion.x = x;
508       xe.xmotion.y = y;
509       xe.xmotion.state = state;
510       break;
511     case KeyPress:
512     case KeyRelease:
513       {
514         NSString *nss = [e characters];
515         const char *s = [nss cStringUsingEncoding:NSUTF8StringEncoding];
516         xe.xkey.keycode = (s && *s ? *s : 0);
517         xe.xkey.state = state;
518         break;
519       }
520     default:
521       abort();
522   }
523
524   [self lockFocus];
525   [self prepareContext];
526   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
527   [self unlockFocus];
528   return result;
529 }
530
531
532 - (void) mouseDown: (NSEvent *) e
533 {
534   if (! [self doEvent:e type:ButtonPress])
535     [super mouseDown:e];
536 }
537
538 - (void) mouseUp: (NSEvent *) e
539 {
540   if (! [self doEvent:e type:ButtonRelease])
541     [super mouseUp:e];
542 }
543
544 - (void) otherMouseDown: (NSEvent *) e
545 {
546   if (! [self doEvent:e type:ButtonPress])
547     [super otherMouseDown:e];
548 }
549
550 - (void) otherMouseUp: (NSEvent *) e
551 {
552   if (! [self doEvent:e type:ButtonRelease])
553     [super otherMouseUp:e];
554 }
555
556 - (void) mouseMoved: (NSEvent *) e
557 {
558   if (! [self doEvent:e type:MotionNotify])
559     [super mouseMoved:e];
560 }
561
562 - (void) mouseDragged: (NSEvent *) e
563 {
564   if (! [self doEvent:e type:MotionNotify])
565     [super mouseDragged:e];
566 }
567
568 - (void) otherMouseDragged: (NSEvent *) e
569 {
570   if (! [self doEvent:e type:MotionNotify])
571     [super otherMouseDragged:e];
572 }
573
574 - (void) scrollWheel: (NSEvent *) e
575 {
576   if (! [self doEvent:e type:ButtonPress])
577     [super scrollWheel:e];
578 }
579
580 - (void) keyDown: (NSEvent *) e
581 {
582   if (! [self doEvent:e type:KeyPress])
583     [super keyDown:e];
584 }
585
586 - (void) keyUp: (NSEvent *) e
587 {
588   if (! [self doEvent:e type:KeyRelease])
589     [super keyUp:e];
590 }
591
592
593 @end
594
595 /* Utility functions...
596  */
597
598 static PrefsReader *
599 get_prefsReader (Display *dpy)
600 {
601   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
602   if (!view) abort();
603   return [view prefsReader];
604 }
605
606
607 char *
608 get_string_resource (Display *dpy, char *name, char *class)
609 {
610   return [get_prefsReader(dpy) getStringResource:name];
611 }
612
613 Bool
614 get_boolean_resource (Display *dpy, char *name, char *class)
615 {
616   return [get_prefsReader(dpy) getBooleanResource:name];
617 }
618
619 int
620 get_integer_resource (Display *dpy, char *name, char *class)
621 {
622   return [get_prefsReader(dpy) getIntegerResource:name];
623 }
624
625 double
626 get_float_resource (Display *dpy, char *name, char *class)
627 {
628   return [get_prefsReader(dpy) getFloatResource:name];
629 }