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