1 /* xscreensaver, Copyright (c) 2006-2013 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 three 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/Resources/ 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.
26 Third, it is the scaffolding which turns a set of screen savers into
27 a single iPhone / iPad program. In that case, all of the savers are
28 linked in to this executable, since iOS does not allow dynamic loading
29 of bundles that have executable code in them. Bleh.
32 #import <TargetConditionals.h>
33 #import "SaverRunner.h"
34 #import "SaverListController.h"
35 #import "XScreenSaverGLView.h"
40 @interface RotateyViewController : UINavigationController
43 @implementation RotateyViewController
44 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
53 @implementation SaverRunner
56 - (ScreenSaverView *) makeSaverView: (NSString *) module
57 withSize: (NSSize) size
63 // Load the XScreenSaverView subclass and code from a ".saver" bundle.
65 NSString *name = [module stringByAppendingPathExtension:@"saver"];
66 NSString *path = [saverDir stringByAppendingPathComponent:name];
68 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
69 NSLog(@"bundle \"%@\" does not exist", path);
73 NSLog(@"Loading %@", path);
75 // NSBundle *obundle = saverBundle;
77 saverBundle = [NSBundle bundleWithPath:path];
79 new_class = [saverBundle principalClass];
81 // Not entirely unsurprisingly, this tends to break the world.
82 // if (obundle && obundle != saverBundle)
87 // Determine whether to create an X11 view or an OpenGL view by
88 // looking for the "gl" tag in the xml file. This is kind of awful.
90 NSString *path = [saverDir
91 stringByAppendingPathComponent:
92 [[[module lowercaseString]
93 stringByReplacingOccurrencesOfString:@" "
95 stringByAppendingPathExtension:@"xml"]];
96 NSString *xml = [NSString stringWithContentsOfFile:path
97 encoding:NSISOLatin1StringEncoding
99 NSAssert (xml, @"no XML: %@", path);
100 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
103 ? [XScreenSaverGLView class]
104 : [XScreenSaverView class]);
106 # endif // USE_IPHONE
112 rect.origin.x = rect.origin.y = 0;
113 rect.size.width = size.width;
114 rect.size.height = size.height;
116 XScreenSaverView *instance =
117 [(XScreenSaverView *) [new_class alloc]
122 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
127 /* KLUGE: Inform the underlying program that we're in "standalone"
128 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
129 This is kind of horrible but I haven't thought of a more sensible
130 way to make this work.
133 if ([saverNames count] == 1) {
134 putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
138 return (ScreenSaverView *) instance;
144 static ScreenSaverView *
145 find_saverView_child (NSView *v)
147 NSArray *kids = [v subviews];
148 int nkids = [kids count];
150 for (i = 0; i < nkids; i++) {
151 NSObject *kid = [kids objectAtIndex:i];
152 if ([kid isKindOfClass:[ScreenSaverView class]]) {
153 return (ScreenSaverView *) kid;
155 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
163 static ScreenSaverView *
164 find_saverView (NSView *v)
167 NSView *p = [v superview];
171 return find_saverView_child (v);
175 /* Changes the contents of the menubar menus to correspond to
176 the running saver. Desktop only.
179 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
181 if ([v isKindOfClass:[NSMenu class]]) {
182 NSMenu *m = (NSMenu *)v;
183 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
184 withString:new_str]];
185 NSArray *kids = [m itemArray];
186 int nkids = [kids count];
188 for (i = 0; i < nkids; i++) {
189 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
191 } else if ([v isKindOfClass:[NSMenuItem class]]) {
192 NSMenuItem *mi = (NSMenuItem *)v;
193 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
194 withString:new_str]];
195 NSMenu *m = [mi submenu];
196 if (m) relabel_menus (m, old_str, new_str);
201 - (void) openPreferences: (id) sender
204 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
205 sv = find_saverView ((NSView *) sender);
209 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
210 w = [windows objectAtIndex:i];
211 if ([w isKeyWindow]) break;
213 sv = find_saverView ([w contentView]);
216 NSAssert (sv, @"no saver view");
218 NSWindow *prefs = [sv configureSheet];
220 [NSApp beginSheet:prefs
221 modalForWindow:[sv window]
223 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
225 int code = [NSApp runModalForWindow:prefs];
227 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
228 We have to restart *both* animations, because the xlockmore-style
229 ones will blow up if one re-inits but the other doesn't.
231 if (code != NSCancelButton) {
232 if ([sv isAnimating])
239 - (void) preferencesClosed: (NSWindow *) sheet
240 returnCode: (int) returnCode
241 contextInfo: (void *) contextInfo
243 [NSApp stopModalWithCode:returnCode];
249 - (UIImage *) screenshot
251 return saved_screenshot;
254 - (void) saveScreenshot
256 // Most of this is from:
257 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
258 // The rotation stuff is by me.
260 CGSize size = [[UIScreen mainScreen] bounds].size;
262 UIInterfaceOrientation orient =
263 [[window rootViewController] interfaceOrientation];
264 if (orient == UIInterfaceOrientationLandscapeLeft ||
265 orient == UIInterfaceOrientationLandscapeRight) {
266 // Rotate the shape of the canvas 90 degrees.
267 double s = size.width;
268 size.width = size.height;
273 // Create a graphics context with the target size
274 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
275 // take the scale into consideration
276 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
278 if (UIGraphicsBeginImageContextWithOptions)
279 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
281 UIGraphicsBeginImageContext (size);
283 CGContextRef ctx = UIGraphicsGetCurrentContext();
286 // Rotate the graphics context to match current hardware rotation.
289 case UIInterfaceOrientationPortraitUpsideDown:
290 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
291 CGContextRotateCTM (ctx, M_PI);
292 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
294 case UIInterfaceOrientationLandscapeLeft:
295 case UIInterfaceOrientationLandscapeRight:
296 CGContextTranslateCTM (ctx,
297 ([window frame].size.height -
298 [window frame].size.width) / 2,
299 ([window frame].size.width -
300 [window frame].size.height) / 2);
301 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
302 CGContextRotateCTM (ctx,
303 (orient == UIInterfaceOrientationLandscapeLeft
306 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
312 // Iterate over every window from back to front
314 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
315 if (![win respondsToSelector:@selector(screen)] ||
316 [win screen] == [UIScreen mainScreen]) {
318 // -renderInContext: renders in the coordinate space of the layer,
319 // so we must first apply the layer's geometry to the graphics context
320 CGContextSaveGState (ctx);
322 // Center the context around the window's anchor point
323 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
325 // Apply the window's transform about the anchor point
326 CGContextConcatCTM (ctx, [win transform]);
328 // Offset by the portion of the bounds left of and above anchor point
329 CGContextTranslateCTM (ctx,
330 -[win bounds].size.width * [[win layer] anchorPoint].x,
331 -[win bounds].size.height * [[win layer] anchorPoint].y);
333 // Render the layer hierarchy to the current context
334 [[win layer] renderInContext:ctx];
336 // Restore the context
337 CGContextRestoreGState (ctx);
341 if (saved_screenshot)
342 [saved_screenshot release];
343 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
345 UIGraphicsEndImageContext();
349 - (void) openPreferences: (NSString *) saver
351 [self loadSaver:saver launch:NO];
352 if (! saverView) return;
354 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
355 [prefs setObject:saver forKey:@"selectedSaverName"];
358 [rootViewController pushViewController: [saverView configureView]
367 - (void)loadSaver:(NSString *)name launch:(BOOL)launch
371 if (saverName && [saverName isEqualToString: name]) {
373 for (NSWindow *win in windows) {
374 ScreenSaverView *sv = find_saverView ([win contentView]);
375 if (![sv isAnimating])
383 for (NSWindow *win in windows) {
384 NSView *cv = [win contentView];
385 NSString *old_title = [win title];
386 if (!old_title) old_title = @"XScreenSaver";
387 [win setTitle: name];
388 relabel_menus (menubar, old_title, name);
390 ScreenSaverView *old_view = find_saverView (cv);
391 NSView *sup = old_view ? [old_view superview] : cv;
394 if ([old_view isAnimating])
395 [old_view stopAnimation];
396 [old_view removeFromSuperview];
399 NSSize size = [cv frame].size;
400 ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
401 NSAssert (new_view, @"unable to make a saver view");
403 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
404 [sup addSubview: new_view];
405 [win makeFirstResponder:new_view];
406 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
409 [new_view startAnimation];
412 NSUserDefaultsController *ctl =
413 [NSUserDefaultsController sharedUserDefaultsController];
418 # if TARGET_IPHONE_SIMULATOR
419 NSLog (@"selecting saver \"%@\"", name);
422 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
423 [prefs setObject:name forKey:@"selectedSaverName"];
426 if (saverName && [saverName isEqualToString: name]) {
427 if ([saverView isAnimating])
435 if (! backgroundView) {
436 // This view is the parent of the XScreenSaverView, and exists only
437 // so that there is a black background behind it. Without this, when
438 // rotation is in progress, the scrolling-list window's corners show
439 // through in the corners.
440 backgroundView = [[[NSView class] alloc] initWithFrame:[window frame]];
441 [backgroundView setBackgroundColor:[NSColor blackColor]];
445 if ([saverView isAnimating])
446 [saverView stopAnimation];
447 [saverView removeFromSuperview];
448 [backgroundView removeFromSuperview];
451 NSSize size = [window frame].size;
452 saverView = [self makeSaverView:name withSize: size];
455 [[[UIAlertView alloc] initWithTitle: name
456 message: @"Unable to load!"
458 cancelButtonTitle: @"Bummer"
459 otherButtonTitles: nil]
464 [saverView setFrame: [window frame]];
466 [[NSNotificationCenter defaultCenter]
467 addObserver:saverView
468 selector:@selector(didRotate:)
469 name:UIDeviceOrientationDidChangeNotification object:nil];
473 [self saveScreenshot];
474 [window addSubview: backgroundView];
475 [backgroundView addSubview: saverView];
476 [saverView becomeFirstResponder];
477 [saverView startAnimation];
478 [self aboutPanel:nil];
480 # endif // USE_IPHONE
484 - (void)loadSaver:(NSString *)name
486 [self loadSaver:name launch:YES];
490 - (void)aboutPanel:(id)sender
494 NSDictionary *bd = [saverBundle infoDictionary];
495 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
497 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
498 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
499 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
500 forKey:@"ApplicationVersion"];
501 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
502 [d setValue:[[NSAttributedString alloc]
503 initWithString: (NSString *)
504 [bd objectForKey:@"CFBundleGetInfoString"]]
507 [[NSApplication sharedApplication]
508 orderFrontStandardAboutPanelWithOptions:d];
511 NSString *name = saverName;
512 NSString *year = [self makeDesc:saverName yearOnly:YES];
515 CGRect frame = [saverView frame];
519 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
520 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
521 CGSize tsize1 = [name sizeWithFont:font1
522 constrainedToSize:CGSizeMake(frame.size.width,
524 CGSize tsize2 = [year sizeWithFont:font2
525 constrainedToSize:CGSizeMake(frame.size.width,
527 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
528 tsize1.width : tsize2.width,
529 tsize1.height + tsize2.height);
531 // Don't know how to find inner margin of UITextView.
533 tsize.width += margin * 4;
534 tsize.height += margin * 2;
536 if ([saverView frame].size.width >= 768)
537 tsize.height += pt1 * 3; // extra bottom margin on iPad
539 frame = CGRectMake (0, 0, tsize.width, tsize.height);
541 UIInterfaceOrientation orient =
542 // Why are both of these wrong when starting up rotated??
543 [[UIDevice currentDevice] orientation];
544 // [rootViewController interfaceOrientation];
546 /* Get the text oriented properly, and move it to the bottom of the
547 screen, since many savers have action in the middle.
550 case UIDeviceOrientationLandscapeRight:
552 frame.origin.x = ([saverView frame].size.width
553 - (tsize.width - tsize.height) / 2
555 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
557 case UIDeviceOrientationLandscapeLeft:
559 frame.origin.x = -(tsize.width - tsize.height) / 2;
560 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
562 case UIDeviceOrientationPortraitUpsideDown:
564 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
569 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
570 frame.origin.y = [saverView frame].size.height - tsize.height;
575 [aboutBox removeFromSuperview];
577 aboutBox = [[UIView alloc] initWithFrame:frame];
579 aboutBox.transform = CGAffineTransformMakeRotation (rot);
580 aboutBox.backgroundColor = [UIColor clearColor];
582 /* There seems to be no easy way to stroke the font, so instead draw
583 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
584 a black shadow to each. (You'd think the shadow alone would be
585 enough, but there's no way to make it dark enough to be legible.)
587 for (int i = 0; i < 5; i++) {
588 UITextView *textview;
590 frame.origin.x = frame.origin.y = 0;
592 case 0: frame.origin.x = -off; break;
593 case 1: frame.origin.x = off; break;
594 case 2: frame.origin.y = -off; break;
595 case 3: frame.origin.y = off; break;
598 for (int j = 0; j < 2; j++) {
600 frame.origin.y = (j == 0 ? 0 : pt1);
601 textview = [[UITextView alloc] initWithFrame:frame];
602 textview.font = (j == 0 ? font1 : font2);
603 textview.text = (j == 0 ? name : year);
604 textview.textAlignment = UITextAlignmentCenter;
605 textview.showsHorizontalScrollIndicator = NO;
606 textview.showsVerticalScrollIndicator = NO;
607 textview.scrollEnabled = NO;
608 textview.editable = NO;
609 textview.userInteractionEnabled = NO;
610 textview.backgroundColor = [UIColor clearColor];
611 textview.textColor = (i == 4
612 ? [UIColor yellowColor]
613 : [UIColor blackColor]);
615 CALayer *textLayer = (CALayer *)
616 [textview.layer.sublayers objectAtIndex:0];
617 textLayer.shadowColor = [UIColor blackColor].CGColor;
618 textLayer.shadowOffset = CGSizeMake(0, 0);
619 textLayer.shadowOpacity = 1;
620 textLayer.shadowRadius = 2;
622 [aboutBox addSubview:textview];
626 CABasicAnimation *anim =
627 [CABasicAnimation animationWithKeyPath:@"opacity"];
629 anim.repeatCount = 1;
630 anim.autoreverses = NO;
631 anim.fromValue = [NSNumber numberWithFloat:0.0];
632 anim.toValue = [NSNumber numberWithFloat:1.0];
633 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
635 [backgroundView addSubview:aboutBox];
638 [splashTimer invalidate];
641 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
643 selector:@selector(aboutOff)
646 # endif // USE_IPHONE
655 [splashTimer invalidate];
658 CABasicAnimation *anim =
659 [CABasicAnimation animationWithKeyPath:@"opacity"];
661 anim.repeatCount = 1;
662 anim.autoreverses = NO;
663 anim.fromValue = [NSNumber numberWithFloat: 1];
664 anim.toValue = [NSNumber numberWithFloat: 0];
665 anim.delegate = self;
666 aboutBox.layer.opacity = 0;
667 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
674 - (void)selectedSaverDidChange:(NSDictionary *)change
676 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
677 NSString *name = [prefs stringForKey:@"selectedSaverName"];
681 if (! [saverNames containsObject:name]) {
682 NSLog (@"saver \"%@\" does not exist", name);
686 [self loadSaver: name];
690 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
693 NSString *ext = @"saver";
695 NSString *ext = @"xml";
698 NSArray *files = [[NSFileManager defaultManager]
699 contentsOfDirectoryAtPath:dir error:nil];
700 if (! files) return 0;
701 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
703 for (NSString *p in files) {
704 if ([[p pathExtension] caseInsensitiveCompare: ext])
708 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
709 # else // !USE_IPHONE
711 // Get the saver name's capitalization right by reading the XML file.
713 p = [dir stringByAppendingPathComponent: p];
714 NSString *name = [NSString stringWithContentsOfFile:p
715 encoding:NSISOLatin1StringEncoding
717 NSRange r = [name rangeOfString:@"_label=\"" options:0];
718 name = [name substringFromIndex: r.location + r.length];
719 r = [name rangeOfString:@"\"" options:0];
720 name = [name substringToIndex: r.location];
722 NSAssert1 (name, @"no name in %@", p);
724 # endif // !USE_IPHONE
726 [result addObject: name];
729 if (! [result count])
737 - (NSArray *) listSaverBundleNames
739 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
742 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
743 // directories in the bundle.
744 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
745 stringByAppendingPathComponent:@"Contents"]
746 stringByAppendingPathComponent:@"Resources"]];
747 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
749 // Also look in the same directory as the executable.
750 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
751 stringByDeletingLastPathComponent]];
753 // Finally, look in standard MacOS screensaver directories.
754 // [dirs addObject: @"~/Library/Screen Savers"];
755 // [dirs addObject: @"/Library/Screen Savers"];
756 // [dirs addObject: @"/System/Library/Screen Savers"];
760 // On iOS, only look in the bundle's root directory.
761 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
763 # endif // USE_IPHONE
766 for (i = 0; i < [dirs count]; i++) {
767 NSString *dir = [dirs objectAtIndex:i];
768 NSArray *names = [self listSaverBundleNamesInDir:dir];
769 if (! names) continue;
770 saverDir = [dir retain];
771 saverNames = [names retain];
775 NSString *err = @"no .saver bundles found in: ";
776 for (i = 0; i < [dirs count]; i++) {
777 if (i) err = [err stringByAppendingString:@", "];
778 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
779 stringByAbbreviatingWithTildeInPath]];
780 err = [err stringByAppendingString:@"/"];
783 return [NSArray array];
787 /* Create the popup menu of available saver names.
791 - (NSPopUpButton *) makeMenu
794 rect.origin.x = rect.origin.y = 0;
795 rect.size.width = 10;
796 rect.size.height = 10;
797 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
801 for (i = 0; i < [saverNames count]; i++) {
802 NSString *name = [saverNames objectAtIndex:i];
803 [popup addItemWithTitle:name];
804 [[popup itemWithTitle:name] setRepresentedObject:name];
806 NSRect r = [popup frame];
807 if (r.size.width > max_width) max_width = r.size.width;
810 // Bind the menu to preferences, and trigger a callback when an item
813 NSString *key = @"values.selectedSaverName";
814 NSUserDefaultsController *prefs =
815 [NSUserDefaultsController sharedUserDefaultsController];
816 [prefs addObserver:self
819 context:@selector(selectedSaverDidChange:)];
820 [popup bind:@"selectedObject"
824 [prefs setAppliesImmediately:YES];
826 NSRect r = [popup frame];
827 r.size.width = max_width;
834 - (NSString *) makeDesc:(NSString *)saver
835 yearOnly:(BOOL) yearp
838 NSString *path = [saverDir stringByAppendingPathComponent:
839 [[saver lowercaseString]
840 stringByReplacingOccurrencesOfString:@" "
844 path = [path stringByAppendingPathExtension:@"xml"];
845 desc = [NSString stringWithContentsOfFile:path
846 encoding:NSISOLatin1StringEncoding
848 if (! desc) goto FAIL;
850 r = [desc rangeOfString:@"<_description>"
851 options:NSCaseInsensitiveSearch];
856 desc = [desc substringFromIndex: r.location + r.length];
857 r = [desc rangeOfString:@"</_description>"
858 options:NSCaseInsensitiveSearch];
860 desc = [desc substringToIndex: r.location];
862 // Leading and trailing whitespace.
863 desc = [desc stringByTrimmingCharactersInSet:
864 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
866 // Let's see if we can find a year on the last line.
867 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
869 for (NSString *word in
870 [[desc substringFromIndex:r.location + r.length]
871 componentsSeparatedByCharactersInSet:
872 [NSCharacterSet characterSetWithCharactersInString:
874 int n = [word doubleValue];
875 if (n > 1970 && n < 2100)
879 // Delete everything after the first blank line.
880 r = [desc rangeOfString:@"\n\n" options:0];
882 desc = [desc substringToIndex: r.location];
884 // Truncate really long ones.
886 if ([desc length] > max)
887 desc = [desc substringToIndex: max];
890 desc = [year stringByAppendingString:
891 [@": " stringByAppendingString: desc]];
894 desc = year ? year : @"";
898 desc = @"Oops, this module appears to be incomplete.";
899 // NSLog(@"broken saver: %@", path);
905 - (NSString *) makeDesc:(NSString *)saver
907 return [self makeDesc:saver yearOnly:NO];
912 /* Create a dictionary of one-line descriptions of every saver,
913 for display on the UITableView.
915 - (NSDictionary *)makeDescTable
917 NSMutableDictionary *dict =
918 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
919 for (NSString *saver in saverNames) {
920 [dict setObject:[self makeDesc:saver] forKey:saver];
930 /* This is called when the "selectedSaverName" pref changes, e.g.,
931 when a menu selection is made.
933 - (void)observeValueForKeyPath:(NSString *)keyPath
935 change:(NSDictionary *)change
936 context:(void *)context
938 SEL dispatchSelector = (SEL)context;
939 if (dispatchSelector != NULL) {
940 [self performSelector:dispatchSelector withObject:change];
942 [super observeValueForKeyPath:keyPath
952 /* Create the desktop window shell, possibly including a preferences button.
954 - (NSWindow *) makeWindow
957 static int count = 0;
958 Bool simple_p = ([saverNames count] == 1);
960 NSPopUpButton *menu = 0;
965 sv_rect.origin.x = sv_rect.origin.y = 0;
966 sv_rect.size.width = 320;
967 sv_rect.size.height = 240;
968 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
969 initWithFrame:sv_rect
972 // make a "Preferences" button
977 rect.size.width = rect.size.height = 10;
978 pb = [[NSButton alloc] initWithFrame:rect];
979 [pb setTitle:@"Preferences"];
980 [pb setBezelStyle:NSRoundedBezelStyle];
983 rect.origin.x = ([sv frame].size.width -
984 [pb frame].size.width) / 2;
985 [pb setFrameOrigin:rect.origin];
990 [pb setAction:@selector(openPreferences:)];
992 // Make a saver selection menu
994 menu = [self makeMenu];
997 [menu setFrameOrigin:rect.origin];
999 // make a box to wrap the saverView
1003 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1004 gbox = [[NSBox alloc] initWithFrame:rect];
1005 rect.size.width = rect.size.height = 10;
1006 [gbox setContentViewMargins:rect.size];
1007 [gbox setTitlePosition:NSNoTitle];
1008 [gbox addSubview:sv];
1011 // make a box to wrap the other two boxes
1013 rect.origin.x = rect.origin.y = 0;
1014 rect.size.width = [gbox frame].size.width;
1015 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1016 pbox = [[NSBox alloc] initWithFrame:rect];
1017 [pbox setTitlePosition:NSNoTitle];
1018 [pbox setBorderType:NSNoBorder];
1019 [pbox addSubview:gbox];
1020 if (menu) [pbox addSubview:menu];
1021 if (pb) [pbox addSubview:pb];
1024 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1025 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1026 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1027 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1030 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1033 // and make a window to hold that.
1035 NSScreen *screen = [NSScreen mainScreen];
1036 rect = pbox ? [pbox frame] : [sv frame];
1037 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1038 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1040 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1042 NSWindow *win = [[NSWindow alloc]
1043 initWithContentRect:rect
1044 styleMask:(NSTitledWindowMask |
1045 NSClosableWindowMask |
1046 NSMiniaturizableWindowMask |
1047 NSResizableWindowMask)
1048 backing:NSBackingStoreBuffered
1051 [win setMinSize:[win frameRectForContentRect:rect].size];
1052 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1054 [win makeKeyAndOrderFront:win];
1056 [sv startAnimation]; // this is the dummy saver
1063 # endif // !USE_IPHONE
1066 - (void)applicationDidFinishLaunching:
1068 (NSNotification *) notif
1069 # else // USE_IPHONE
1070 (UIApplication *) application
1071 # endif // USE_IPHONE
1073 [self listSaverBundleNames];
1076 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1077 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1082 // Create either one window (for standalone, e.g. Phosphor.app)
1083 // or two windows for SaverTester.app.
1084 for (i = 0; i < window_count; i++) {
1085 NSWindow *win = [self makeWindow];
1086 // Get the last-saved window position out of preferences.
1087 [win setFrameAutosaveName:
1088 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1089 [win setFrameUsingName:[win frameAutosaveName]];
1092 # else // USE_IPHONE
1094 # undef ya_rand_init
1095 ya_rand_init (0); // Now's a good time.
1097 rootViewController = [[[RotateyViewController alloc] init] retain];
1098 [window setRootViewController: rootViewController];
1100 SaverListController *menu = [[SaverListController alloc]
1101 initWithNames:saverNames
1102 descriptions:[self makeDescTable]];
1103 [rootViewController pushViewController:menu animated:YES];
1104 [menu becomeFirstResponder];
1106 [window makeKeyAndVisible];
1107 [window setAutoresizesSubviews:YES];
1108 [window setAutoresizingMask:
1109 (UIViewAutoresizingFlexibleWidth |
1110 UIViewAutoresizingFlexibleHeight)];
1112 application.applicationSupportsShakeToEdit = YES;
1114 # endif // USE_IPHONE
1116 NSString *forced = 0;
1117 /* In the XCode project, each .saver scheme sets this env var when
1118 launching SaverTester.app so that it knows which one we are
1119 currently debugging. If this is set, it overrides the default
1120 selection in the popup menu. If unset, that menu persists to
1121 whatever it was last time.
1123 const char *f = getenv ("SELECTED_SAVER");
1125 forced = [NSString stringWithCString:(char *)f
1126 encoding:NSUTF8StringEncoding];
1128 if (forced && ![saverNames containsObject:forced]) {
1129 NSLog(@"forced saver \"%@\" does not exist", forced);
1133 // If there's only one saver, run that.
1134 if (!forced && [saverNames count] == 1)
1135 forced = [saverNames objectAtIndex:0];
1137 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1140 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1145 // If nothing was selected (e.g., this is the first launch)
1146 // then scroll randomly instead of starting up at "A".
1149 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1152 [menu scrollTo: prev];
1153 # endif // USE_IPHONE
1156 [prefs setObject:forced forKey:@"selectedSaverName"];
1159 /* Don't auto-launch the saver unless it was running last time.
1160 XScreenSaverView manages this, on crash_timer.
1163 if (!forced && ![prefs boolForKey:@"wasRunning"])
1167 [self selectedSaverDidChange:nil];
1173 /* When the window closes, exit (even if prefs still open.)
1175 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1180 # else // USE_IPHONE
1182 - (void)applicationWillResignActive:(UIApplication *)app
1184 [(XScreenSaverView *)view setScreenLocked:YES];
1187 - (void)applicationDidBecomeActive:(UIApplication *)app
1189 [(XScreenSaverView *)view setScreenLocked:NO];
1192 - (void)applicationDidEnterBackground:(UIApplication *)application
1194 [(XScreenSaverView *)view setScreenLocked:YES];
1197 #endif // USE_IPHONE