From http://www.jwz.org/xscreensaver/xscreensaver-5.15.tar.gz
[xscreensaver] / OSX / SaverRunner.m
1 /* xscreensaver, Copyright (c) 2006-2011 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 program serves two purposes:
13
14    First, It is a test harness for screen savers.  When it launches, it
15    looks around for .saver bundles (in the current directory, and then in
16    the standard directories) and puts up a pair of windows that allow you
17    to select the saver to run.  This is less clicking than running them
18    through System Preferences.  This is the "SaverTester.app" program.
19
20    Second, it can be used to transform any screen saver into a standalone
21    program.  Just put one (and only one) .saver bundle into the app
22    bundle's Contents/PlugIns/ directory, and it will load and run that
23    saver at start-up (without the saver-selection menu or other chrome).
24    This is how the "Phosphor.app" and "Apple2.app" programs work.
25  */
26
27 #import "SaverRunner.h"
28 #import "XScreenSaverGLView.h"
29
30 @implementation SaverRunner
31
32 - (ScreenSaverView *) makeSaverView: (NSString *) module
33 {
34   NSString *name = [module stringByAppendingPathExtension:@"saver"];
35   NSString *path = [saverDir stringByAppendingPathComponent:name];
36   saverBundle = [NSBundle bundleWithPath:path];
37   Class new_class = [saverBundle principalClass];
38   NSAssert1 (new_class, @"unable to load \"%@\"", path);
39
40
41   NSRect rect;
42   rect.origin.x = rect.origin.y = 0;
43   rect.size.width = 320;
44   rect.size.height = 240;
45
46   id instance = [[new_class alloc] initWithFrame:rect isPreview:YES];
47   NSAssert1 (instance, @"unable to instantiate %@", new_class);
48
49
50   /* KLUGE: Inform the underlying program that we're in "standalone"
51      mode.  This is kind of horrible but I haven't thought of a more
52      sensible way to make this work.
53    */
54   if ([saverNames count] == 1) {
55     putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
56   }
57
58   return (ScreenSaverView *) instance;
59 }
60
61
62 static ScreenSaverView *
63 find_saverView_child (NSView *v)
64 {
65   NSArray *kids = [v subviews];
66   int nkids = [kids count];
67   int i;
68   for (i = 0; i < nkids; i++) {
69     NSObject *kid = [kids objectAtIndex:i];
70     if ([kid isKindOfClass:[ScreenSaverView class]]) {
71       return (ScreenSaverView *) kid;
72     } else {
73       ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
74       if (sv) return sv;
75     }
76   }
77   return 0;
78 }
79
80
81 static ScreenSaverView *
82 find_saverView (NSView *v)
83 {
84   while (1) {
85     NSView *p = [v superview];
86     if (p) v = p;
87     else break;
88   }
89   return find_saverView_child (v);
90 }
91
92
93 static void
94 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
95 {
96   if ([v isKindOfClass:[NSMenu class]]) {
97     NSMenu *m = (NSMenu *)v;
98     [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
99                             withString:new_str]];
100     NSArray *kids = [m itemArray];
101     int nkids = [kids count];
102     int i;
103     for (i = 0; i < nkids; i++) {
104       relabel_menus ([kids objectAtIndex:i], old_str, new_str);
105     }
106   } else if ([v isKindOfClass:[NSMenuItem class]]) {
107     NSMenuItem *mi = (NSMenuItem *)v;
108     [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
109                               withString:new_str]];
110     NSMenu *m = [mi submenu];
111     if (m) relabel_menus (m, old_str, new_str);
112   }
113 }
114
115
116 - (void) openPreferences: (id) sender
117 {
118   ScreenSaverView *sv;
119
120   if ([sender isKindOfClass:[NSView class]]) {  // Sent from button
121     sv = find_saverView ((NSView *) sender);
122   } else {
123     int i;
124     NSWindow *w = 0;
125     for (i = [windows count]-1; i >= 0; i--) {  // Sent from menubar
126       w = [windows objectAtIndex:i];
127       if ([w isKeyWindow]) break;
128     }
129     sv = find_saverView ([w contentView]);
130   }
131
132   NSAssert (sv, @"no saver view");
133   NSWindow *prefs = [sv configureSheet];
134
135   [NSApp beginSheet:prefs
136      modalForWindow:[sv window]
137       modalDelegate:self
138      didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
139         contextInfo:nil];
140   int code = [NSApp runModalForWindow:prefs];
141   
142   /* Restart the animation if the "OK" button was hit, but not if "Cancel".
143      We have to restart *both* animations, because the xlockmore-style
144      ones will blow up if one re-inits but the other doesn't.
145    */
146   if (code != NSCancelButton) {
147     [sv stopAnimation];
148     [sv startAnimation];
149   }
150 }
151
152 - (void) preferencesClosed: (NSWindow *) sheet
153                 returnCode: (int) returnCode
154                contextInfo: (void  *) contextInfo
155 {
156   [NSApp stopModalWithCode:returnCode];
157 }
158
159
160 - (void)loadSaver:(NSString *)name
161 {
162   int i;
163   for (i = 0; i < [windows count]; i++) {
164     NSWindow *window = [windows objectAtIndex:i];
165     NSView *cv = [window contentView];
166     ScreenSaverView *old_view = find_saverView (cv);
167     NSView *sup = [old_view superview];
168
169     NSString *old_title = [window title];
170     if (!old_title) old_title = @"XScreenSaver";
171     [window setTitle: name];
172     relabel_menus (menubar, old_title, name);
173
174     [old_view stopAnimation];
175     [old_view removeFromSuperview];
176
177     ScreenSaverView *new_view = [self makeSaverView:name];
178     [new_view setFrame: [old_view frame]];
179     [sup addSubview: new_view];
180     [window makeFirstResponder:new_view];
181     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
182     [new_view startAnimation];
183   }
184
185   NSUserDefaultsController *ctl =
186     [NSUserDefaultsController sharedUserDefaultsController];
187   [ctl save:self];
188 }
189
190
191 - (void)aboutPanel:(id)sender
192 {
193   NSDictionary *bd = [saverBundle infoDictionary];
194   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
195
196   [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
197   [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
198   [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
199      forKey:@"ApplicationVersion"];
200   [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
201   [d setValue:[[NSAttributedString alloc]
202                 initWithString: (NSString *) 
203                   [bd objectForKey:@"CFBundleGetInfoString"]]
204      forKey:@"Credits"];
205
206   [[NSApplication sharedApplication]
207     orderFrontStandardAboutPanelWithOptions:d];
208 }
209
210
211
212 - (void)selectedSaverDidChange:(NSDictionary *)change
213 {
214   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
215   NSString *name = [prefs stringForKey:@"selectedSaverName"];
216
217   if (! [saverNames containsObject:name]) {
218     NSLog (@"Saver \"%@\" does not exist", name);
219     return;
220   }
221
222   if (name) [self loadSaver: name];
223 }
224
225
226 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
227 {
228   NSArray *files = [[NSFileManager defaultManager]
229                      contentsOfDirectoryAtPath:dir error:nil];
230   if (! files) return 0;
231
232   int n = [files count];
233   NSMutableArray *result = [NSMutableArray arrayWithCapacity: n+1];
234
235   int i;
236   for (i = 0; i < n; i++) {
237     NSString *p = [files objectAtIndex:i];
238     if ([[p pathExtension] caseInsensitiveCompare:@"saver"]) 
239       continue;
240     [result addObject: [[p lastPathComponent] stringByDeletingPathExtension]];
241   }
242
243   return result;
244 }
245
246
247 - (NSArray *) listSaverBundleNames
248 {
249   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
250
251   // First look in the bundle itself.
252   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
253
254   // Then look in the same directory as the executable.
255   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
256                      stringByDeletingLastPathComponent]];
257
258   // Then look in standard screensaver directories.
259   [dirs addObject: @"~/Library/Screen Savers"];
260   [dirs addObject: @"/Library/Screen Savers"];
261   [dirs addObject: @"/System/Library/Screen Savers"];
262
263   int i;
264   for (i = 0; i < [dirs count]; i++) {
265     NSString *dir = [dirs objectAtIndex:i];
266     NSArray *names = [self listSaverBundleNamesInDir:dir];
267     if (! names) continue;
268
269     // Make sure this directory is on $PATH.
270
271     const char *cdir = [dir cStringUsingEncoding:NSUTF8StringEncoding];
272     const char *opath = getenv ("PATH");
273     if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
274     char *npath = (char *) malloc (strlen (opath) + strlen (cdir) + 30);
275     strcpy (npath, "PATH=");
276     strcat (npath, cdir);
277     strcat (npath, ":");
278     strcat (npath, opath);
279     if (putenv (npath)) {
280       perror ("putenv");
281       abort();
282     }
283     /* Don't free (npath) -- MacOS's putenv() does not copy it. */
284
285     saverDir   = [dir retain];
286     saverNames = [names retain];
287
288     return names;
289   }
290
291   NSString *err = @"no .saver bundles found in: ";
292   for (i = 0; i < [dirs count]; i++) {
293     if (i) err = [err stringByAppendingString:@", "];
294     err = [err stringByAppendingString:[[dirs objectAtIndex:i] 
295                                          stringByAbbreviatingWithTildeInPath]];
296     err = [err stringByAppendingString:@"/"];
297   }
298   NSLog (@"%@", err);
299   exit (1);
300 }
301
302
303 - (NSPopUpButton *) makeMenu
304 {
305   NSRect rect;
306   rect.origin.x = rect.origin.y = 0;
307   rect.size.width = 10;
308   rect.size.height = 10;
309   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
310                                                     pullsDown:NO];
311   int i;
312   float max_width = 0;
313   for (i = 0; i < [saverNames count]; i++) {
314     NSString *name = [saverNames objectAtIndex:i];
315     [popup addItemWithTitle:name];
316     [[popup itemWithTitle:name] setRepresentedObject:name];
317     [popup sizeToFit];
318     NSRect r = [popup frame];
319     if (r.size.width > max_width) max_width = r.size.width;
320   }
321
322   // Bind the menu to preferences, and trigger a callback when an item
323   // is selected.
324   //
325   NSString *key = @"values.selectedSaverName";
326   NSUserDefaultsController *prefs =
327     [NSUserDefaultsController sharedUserDefaultsController];
328   [prefs addObserver:self
329          forKeyPath:key
330             options:0
331             context:@selector(selectedSaverDidChange:)];
332   [popup   bind:@"selectedObject"
333        toObject:prefs
334     withKeyPath:key
335         options:nil];
336   [prefs setAppliesImmediately:YES];
337
338   NSRect r = [popup frame];
339   r.size.width = max_width;
340   [popup setFrame:r];
341   return popup;
342 }
343
344
345 /* This is called when the "selectedSaverName" pref changes, e.g.,
346    when a menu selection is made.
347  */
348 - (void)observeValueForKeyPath:(NSString *)keyPath
349                       ofObject:(id)object
350                         change:(NSDictionary *)change
351                        context:(void *)context
352 {
353   SEL dispatchSelector = (SEL)context;
354   if (dispatchSelector != NULL) {
355     [self performSelector:dispatchSelector withObject:change];
356   } else {
357     [super observeValueForKeyPath:keyPath
358                          ofObject:object
359                            change:change
360                           context:context];
361   }
362 }
363
364
365 - (NSWindow *) makeWindow
366 {
367   NSRect rect;
368   static int count = 0;
369   Bool simple_p = ([saverNames count] == 1);
370   NSButton *pb = 0;
371   NSPopUpButton *menu = 0;
372   NSBox *gbox = 0;
373   NSBox *pbox = 0;
374
375   NSRect sv_rect;
376   sv_rect.origin.x = sv_rect.origin.y = 0;
377   sv_rect.size.width = 320;
378   sv_rect.size.height = 240;
379   ScreenSaverView *sv = [[ScreenSaverView alloc]  // dummy placeholder
380                           initWithFrame:sv_rect
381                           isPreview:YES];
382
383   // make a "Preferences" button
384   //
385   if (! simple_p) {
386     rect.origin.x = 0;
387     rect.origin.y = 0;
388     rect.size.width = rect.size.height = 10;
389     pb = [[NSButton alloc] initWithFrame:rect];
390     [pb setTitle:@"Preferences"];
391     [pb setBezelStyle:NSRoundedBezelStyle];
392     [pb sizeToFit];
393
394     rect.origin.x = ([sv frame].size.width -
395                      [pb frame].size.width) / 2;
396     [pb setFrameOrigin:rect.origin];
397   
398     // grab the click
399     //
400     [pb setTarget:self];
401     [pb setAction:@selector(openPreferences:)];
402
403     // Make a saver selection menu
404     //
405     menu = [self makeMenu];
406     rect.origin.x = 2;
407     rect.origin.y = 2;
408     [menu setFrameOrigin:rect.origin];
409
410     // make a box to wrap the saverView
411     //
412     rect = [sv frame];
413     rect.origin.x = 0;
414     rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
415     gbox = [[NSBox alloc] initWithFrame:rect];
416     rect.size.width = rect.size.height = 10;
417     [gbox setContentViewMargins:rect.size];
418     [gbox setTitlePosition:NSNoTitle];
419     [gbox addSubview:sv];
420     [gbox sizeToFit];
421
422     // make a box to wrap the other two boxes
423     //
424     rect.origin.x = rect.origin.y = 0;
425     rect.size.width  = [gbox frame].size.width;
426     rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
427     pbox = [[NSBox alloc] initWithFrame:rect];
428     [pbox setTitlePosition:NSNoTitle];
429     [pbox setBorderType:NSNoBorder];
430     [pbox addSubview:gbox];
431     if (menu) [pbox addSubview:menu];
432     if (pb)   [pbox addSubview:pb];
433     [pbox sizeToFit];
434
435     [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
436     [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
437     [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
438     [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
439   }
440
441   [sv     setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
442
443
444   // and make a window to hold that.
445   //
446   NSScreen *screen = [NSScreen mainScreen];
447   rect = pbox ? [pbox frame] : [sv frame];
448   rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
449   rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
450   
451   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
452   
453   NSWindow *window = [[NSWindow alloc]
454                       initWithContentRect:rect
455                                 styleMask:(NSTitledWindowMask |
456                                            NSClosableWindowMask |
457                                            NSMiniaturizableWindowMask |
458                                            NSResizableWindowMask)
459                                   backing:NSBackingStoreBuffered
460                                     defer:YES
461                                    screen:screen];
462   [window setMinSize:[window frameRectForContentRect:rect].size];
463
464   [[window contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
465
466   [window makeKeyAndOrderFront:window];
467   
468   [sv startAnimation]; // this is the dummy saver
469
470   count++;
471
472   return window;
473 }
474
475
476 - (void)applicationDidFinishLaunching: (NSNotification *) notif
477 {
478   [self listSaverBundleNames];
479
480   int n = ([saverNames count] == 1 ? 1 : 2);
481   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: n+1] retain];
482   windows = a;
483   int i;
484   for (i = 0; i < n; i++) {
485     NSWindow *window = [self makeWindow];
486     // Get the last-saved window position out of preferences.
487     [window setFrameAutosaveName:
488               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
489     [window setFrameUsingName:[window frameAutosaveName]];
490     [a addObject: window];
491   }
492
493   if (n == 1) {
494     [self loadSaver:[saverNames objectAtIndex:0]];
495   } else {
496
497     /* In the XCode project, each .saver scheme sets this env var when
498        launching SaverTester.app so that it knows which one we are
499        currently debugging.  If this is set, it overrides the default
500        selection in the popup menu.  If unset, that menu persists to
501        whatever it was last time.
502      */
503     const char *forced = getenv ("SELECTED_SAVER");
504     if (forced && *forced) {
505       NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
506       NSString *s = [NSString stringWithCString:(char *)forced
507                               encoding:NSUTF8StringEncoding];
508       NSLog (@"selecting saver %@", s);
509       [prefs setObject:s forKey:@"selectedSaverName"];
510     }
511
512     [self selectedSaverDidChange:nil];
513   }
514 }
515
516
517 /* When the window closes, exit (even if prefs still open.)
518 */
519 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
520 {
521   return YES;
522 }
523
524 @end