1 /* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski <jwz@jwz.org>
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
12 /* This program serves two purposes:
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.
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.
27 #import "SaverRunner.h"
28 #import "XScreenSaverGLView.h"
30 @implementation SaverRunner
32 - (ScreenSaverView *) makeSaverView: (NSString *) module
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);
42 rect.origin.x = rect.origin.y = 0;
43 rect.size.width = 320;
44 rect.size.height = 240;
46 id instance = [[new_class alloc] initWithFrame:rect isPreview:YES];
47 NSAssert1 (instance, @"unable to instantiate %@", new_class);
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.
54 if ([saverNames count] == 1) {
55 putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
58 return (ScreenSaverView *) instance;
62 static ScreenSaverView *
63 find_saverView_child (NSView *v)
65 NSArray *kids = [v subviews];
66 int nkids = [kids count];
68 for (i = 0; i < nkids; i++) {
69 NSObject *kid = [kids objectAtIndex:i];
70 if ([kid isKindOfClass:[ScreenSaverView class]]) {
71 return (ScreenSaverView *) kid;
73 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
81 static ScreenSaverView *
82 find_saverView (NSView *v)
85 NSView *p = [v superview];
89 return find_saverView_child (v);
94 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
96 if ([v isKindOfClass:[NSMenu class]]) {
97 NSMenu *m = (NSMenu *)v;
98 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
100 NSArray *kids = [m itemArray];
101 int nkids = [kids count];
103 for (i = 0; i < nkids; i++) {
104 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
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);
116 - (void) openPreferences: (id) sender
120 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
121 sv = find_saverView ((NSView *) sender);
125 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
126 w = [windows objectAtIndex:i];
127 if ([w isKeyWindow]) break;
129 sv = find_saverView ([w contentView]);
132 NSAssert (sv, @"no saver view");
133 NSWindow *prefs = [sv configureSheet];
135 [NSApp beginSheet:prefs
136 modalForWindow:[sv window]
138 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
140 int code = [NSApp runModalForWindow:prefs];
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.
146 if (code != NSCancelButton) {
152 - (void) preferencesClosed: (NSWindow *) sheet
153 returnCode: (int) returnCode
154 contextInfo: (void *) contextInfo
156 [NSApp stopModalWithCode:returnCode];
160 - (void)loadSaver:(NSString *)name
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];
169 NSString *old_title = [window title];
170 if (!old_title) old_title = @"XScreenSaver";
171 [window setTitle: name];
172 relabel_menus (menubar, old_title, name);
174 [old_view stopAnimation];
175 [old_view removeFromSuperview];
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];
185 NSUserDefaultsController *ctl =
186 [NSUserDefaultsController sharedUserDefaultsController];
191 - (void)aboutPanel:(id)sender
193 NSDictionary *bd = [saverBundle infoDictionary];
194 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
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"]]
206 [[NSApplication sharedApplication]
207 orderFrontStandardAboutPanelWithOptions:d];
212 - (void)selectedSaverDidChange:(NSDictionary *)change
214 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
215 NSString *name = [prefs stringForKey:@"selectedSaverName"];
217 if (! [saverNames containsObject:name]) {
218 NSLog (@"Saver \"%@\" does not exist", name);
222 if (name) [self loadSaver: name];
226 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
228 NSArray *files = [[NSFileManager defaultManager]
229 contentsOfDirectoryAtPath:dir error:nil];
230 if (! files) return 0;
232 int n = [files count];
233 NSMutableArray *result = [NSMutableArray arrayWithCapacity: n+1];
236 for (i = 0; i < n; i++) {
237 NSString *p = [files objectAtIndex:i];
238 if ([[p pathExtension] caseInsensitiveCompare:@"saver"])
240 [result addObject: [[p lastPathComponent] stringByDeletingPathExtension]];
247 - (NSArray *) listSaverBundleNames
249 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
251 // First look in the bundle itself.
252 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
254 // Then look in the same directory as the executable.
255 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
256 stringByDeletingLastPathComponent]];
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"];
264 for (i = 0; i < [dirs count]; i++) {
265 NSString *dir = [dirs objectAtIndex:i];
266 NSArray *names = [self listSaverBundleNamesInDir:dir];
267 if (! names) continue;
269 // Make sure this directory is on $PATH.
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);
278 strcat (npath, opath);
279 if (putenv (npath)) {
283 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
285 saverDir = [dir retain];
286 saverNames = [names retain];
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:@"/"];
303 - (NSPopUpButton *) makeMenu
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
313 for (i = 0; i < [saverNames count]; i++) {
314 NSString *name = [saverNames objectAtIndex:i];
315 [popup addItemWithTitle:name];
316 [[popup itemWithTitle:name] setRepresentedObject:name];
318 NSRect r = [popup frame];
319 if (r.size.width > max_width) max_width = r.size.width;
322 // Bind the menu to preferences, and trigger a callback when an item
325 NSString *key = @"values.selectedSaverName";
326 NSUserDefaultsController *prefs =
327 [NSUserDefaultsController sharedUserDefaultsController];
328 [prefs addObserver:self
331 context:@selector(selectedSaverDidChange:)];
332 [popup bind:@"selectedObject"
336 [prefs setAppliesImmediately:YES];
338 NSRect r = [popup frame];
339 r.size.width = max_width;
345 /* This is called when the "selectedSaverName" pref changes, e.g.,
346 when a menu selection is made.
348 - (void)observeValueForKeyPath:(NSString *)keyPath
350 change:(NSDictionary *)change
351 context:(void *)context
353 SEL dispatchSelector = (SEL)context;
354 if (dispatchSelector != NULL) {
355 [self performSelector:dispatchSelector withObject:change];
357 [super observeValueForKeyPath:keyPath
365 - (NSWindow *) makeWindow
368 static int count = 0;
369 Bool simple_p = ([saverNames count] == 1);
371 NSPopUpButton *menu = 0;
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
383 // make a "Preferences" button
388 rect.size.width = rect.size.height = 10;
389 pb = [[NSButton alloc] initWithFrame:rect];
390 [pb setTitle:@"Preferences"];
391 [pb setBezelStyle:NSRoundedBezelStyle];
394 rect.origin.x = ([sv frame].size.width -
395 [pb frame].size.width) / 2;
396 [pb setFrameOrigin:rect.origin];
401 [pb setAction:@selector(openPreferences:)];
403 // Make a saver selection menu
405 menu = [self makeMenu];
408 [menu setFrameOrigin:rect.origin];
410 // make a box to wrap the saverView
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];
422 // make a box to wrap the other two boxes
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];
435 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
436 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
437 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
438 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
441 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
444 // and make a window to hold that.
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;
451 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
453 NSWindow *window = [[NSWindow alloc]
454 initWithContentRect:rect
455 styleMask:(NSTitledWindowMask |
456 NSClosableWindowMask |
457 NSMiniaturizableWindowMask |
458 NSResizableWindowMask)
459 backing:NSBackingStoreBuffered
462 [window setMinSize:[window frameRectForContentRect:rect].size];
464 [[window contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
466 [window makeKeyAndOrderFront:window];
468 [sv startAnimation]; // this is the dummy saver
476 - (void)applicationDidFinishLaunching: (NSNotification *) notif
478 [self listSaverBundleNames];
480 int n = ([saverNames count] == 1 ? 1 : 2);
481 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: n+1] retain];
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];
494 [self loadSaver:[saverNames objectAtIndex:0]];
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.
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"];
512 [self selectedSaverDidChange:nil];
517 /* When the window closes, exit (even if prefs still open.)
519 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n