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 NSData *xmld = [NSData dataWithContentsOfFile:path];
97 NSAssert (xmld, @"no XML: %@", path);
98 NSString *xml = [XScreenSaverView decompressXML:xmld];
99 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
102 ? [XScreenSaverGLView class]
103 : [XScreenSaverView class]);
105 # endif // USE_IPHONE
111 rect.origin.x = rect.origin.y = 0;
112 rect.size.width = size.width;
113 rect.size.height = size.height;
115 XScreenSaverView *instance =
116 [(XScreenSaverView *) [new_class alloc]
121 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
126 /* KLUGE: Inform the underlying program that we're in "standalone"
127 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
128 This is kind of horrible but I haven't thought of a more sensible
129 way to make this work.
132 if ([saverNames count] == 1) {
133 putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
137 return (ScreenSaverView *) instance;
143 static ScreenSaverView *
144 find_saverView_child (NSView *v)
146 NSArray *kids = [v subviews];
147 int nkids = [kids count];
149 for (i = 0; i < nkids; i++) {
150 NSObject *kid = [kids objectAtIndex:i];
151 if ([kid isKindOfClass:[ScreenSaverView class]]) {
152 return (ScreenSaverView *) kid;
154 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
162 static ScreenSaverView *
163 find_saverView (NSView *v)
166 NSView *p = [v superview];
170 return find_saverView_child (v);
174 /* Changes the contents of the menubar menus to correspond to
175 the running saver. Desktop only.
178 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
180 if ([v isKindOfClass:[NSMenu class]]) {
181 NSMenu *m = (NSMenu *)v;
182 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
183 withString:new_str]];
184 NSArray *kids = [m itemArray];
185 int nkids = [kids count];
187 for (i = 0; i < nkids; i++) {
188 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
190 } else if ([v isKindOfClass:[NSMenuItem class]]) {
191 NSMenuItem *mi = (NSMenuItem *)v;
192 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
193 withString:new_str]];
194 NSMenu *m = [mi submenu];
195 if (m) relabel_menus (m, old_str, new_str);
200 - (void) openPreferences: (id) sender
203 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
204 sv = find_saverView ((NSView *) sender);
208 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
209 w = [windows objectAtIndex:i];
210 if ([w isKeyWindow]) break;
212 sv = find_saverView ([w contentView]);
215 NSAssert (sv, @"no saver view");
217 NSWindow *prefs = [sv configureSheet];
219 [NSApp beginSheet:prefs
220 modalForWindow:[sv window]
222 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
224 int code = [NSApp runModalForWindow:prefs];
226 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
227 We have to restart *both* animations, because the xlockmore-style
228 ones will blow up if one re-inits but the other doesn't.
230 if (code != NSCancelButton) {
231 if ([sv isAnimating])
238 - (void) preferencesClosed: (NSWindow *) sheet
239 returnCode: (int) returnCode
240 contextInfo: (void *) contextInfo
242 [NSApp stopModalWithCode:returnCode];
248 - (UIImage *) screenshot
250 return saved_screenshot;
253 - (void) saveScreenshot
255 // Most of this is from:
256 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
257 // The rotation stuff is by me.
259 CGSize size = [[UIScreen mainScreen] bounds].size;
261 UIInterfaceOrientation orient =
262 [[window rootViewController] interfaceOrientation];
263 if (orient == UIInterfaceOrientationLandscapeLeft ||
264 orient == UIInterfaceOrientationLandscapeRight) {
265 // Rotate the shape of the canvas 90 degrees.
266 double s = size.width;
267 size.width = size.height;
272 // Create a graphics context with the target size
273 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
274 // take the scale into consideration
275 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
277 if (UIGraphicsBeginImageContextWithOptions)
278 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
280 UIGraphicsBeginImageContext (size);
282 CGContextRef ctx = UIGraphicsGetCurrentContext();
285 // Rotate the graphics context to match current hardware rotation.
288 case UIInterfaceOrientationPortraitUpsideDown:
289 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
290 CGContextRotateCTM (ctx, M_PI);
291 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
293 case UIInterfaceOrientationLandscapeLeft:
294 case UIInterfaceOrientationLandscapeRight:
295 CGContextTranslateCTM (ctx,
296 ([window frame].size.height -
297 [window frame].size.width) / 2,
298 ([window frame].size.width -
299 [window frame].size.height) / 2);
300 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
301 CGContextRotateCTM (ctx,
302 (orient == UIInterfaceOrientationLandscapeLeft
305 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
311 // Iterate over every window from back to front
313 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
314 if (![win respondsToSelector:@selector(screen)] ||
315 [win screen] == [UIScreen mainScreen]) {
317 // -renderInContext: renders in the coordinate space of the layer,
318 // so we must first apply the layer's geometry to the graphics context
319 CGContextSaveGState (ctx);
321 // Center the context around the window's anchor point
322 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
324 // Apply the window's transform about the anchor point
325 CGContextConcatCTM (ctx, [win transform]);
327 // Offset by the portion of the bounds left of and above anchor point
328 CGContextTranslateCTM (ctx,
329 -[win bounds].size.width * [[win layer] anchorPoint].x,
330 -[win bounds].size.height * [[win layer] anchorPoint].y);
332 // Render the layer hierarchy to the current context
333 [[win layer] renderInContext:ctx];
335 // Restore the context
336 CGContextRestoreGState (ctx);
340 if (saved_screenshot)
341 [saved_screenshot release];
342 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
344 UIGraphicsEndImageContext();
348 - (void) openPreferences: (NSString *) saver
350 [self loadSaver:saver launch:NO];
351 if (! saverView) return;
353 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
354 [prefs setObject:saver forKey:@"selectedSaverName"];
357 [rootViewController pushViewController: [saverView configureView]
366 - (void)loadSaver:(NSString *)name launch:(BOOL)launch
370 if (saverName && [saverName isEqualToString: name]) {
372 for (NSWindow *win in windows) {
373 ScreenSaverView *sv = find_saverView ([win contentView]);
374 if (![sv isAnimating])
382 for (NSWindow *win in windows) {
383 NSView *cv = [win contentView];
384 NSString *old_title = [win title];
385 if (!old_title) old_title = @"XScreenSaver";
386 [win setTitle: name];
387 relabel_menus (menubar, old_title, name);
389 ScreenSaverView *old_view = find_saverView (cv);
390 NSView *sup = old_view ? [old_view superview] : cv;
393 if ([old_view isAnimating])
394 [old_view stopAnimation];
395 [old_view removeFromSuperview];
398 NSSize size = [cv frame].size;
399 ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
400 NSAssert (new_view, @"unable to make a saver view");
402 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
403 [sup addSubview: new_view];
404 [win makeFirstResponder:new_view];
405 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
408 [new_view startAnimation];
411 NSUserDefaultsController *ctl =
412 [NSUserDefaultsController sharedUserDefaultsController];
417 # if TARGET_IPHONE_SIMULATOR
418 NSLog (@"selecting saver \"%@\"", name);
421 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
422 [prefs setObject:name forKey:@"selectedSaverName"];
425 if (saverName && [saverName isEqualToString: name]) {
426 if ([saverView isAnimating])
434 if (! backgroundView) {
435 // This view is the parent of the XScreenSaverView, and exists only
436 // so that there is a black background behind it. Without this, when
437 // rotation is in progress, the scrolling-list window's corners show
438 // through in the corners.
439 backgroundView = [[[NSView class] alloc] initWithFrame:[window frame]];
440 [backgroundView setBackgroundColor:[NSColor blackColor]];
444 if ([saverView isAnimating])
445 [saverView stopAnimation];
446 [saverView removeFromSuperview];
447 [backgroundView removeFromSuperview];
450 NSSize size = [window frame].size;
451 saverView = [self makeSaverView:name withSize: size];
454 [[[UIAlertView alloc] initWithTitle: name
455 message: @"Unable to load!"
457 cancelButtonTitle: @"Bummer"
458 otherButtonTitles: nil]
463 [saverView setFrame: [window frame]];
465 [[NSNotificationCenter defaultCenter]
466 addObserver:saverView
467 selector:@selector(didRotate:)
468 name:UIDeviceOrientationDidChangeNotification object:nil];
472 [self saveScreenshot];
473 [window addSubview: backgroundView];
474 [backgroundView addSubview: saverView];
475 [saverView becomeFirstResponder];
476 [saverView startAnimation];
477 [self aboutPanel:nil];
479 # endif // USE_IPHONE
483 - (void)loadSaver:(NSString *)name
485 [self loadSaver:name launch:YES];
489 - (void)aboutPanel:(id)sender
493 NSDictionary *bd = [saverBundle infoDictionary];
494 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
496 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
497 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
498 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
499 forKey:@"ApplicationVersion"];
500 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
501 [d setValue:[[NSAttributedString alloc]
502 initWithString: (NSString *)
503 [bd objectForKey:@"CFBundleGetInfoString"]]
506 [[NSApplication sharedApplication]
507 orderFrontStandardAboutPanelWithOptions:d];
510 if ([saverNames count] == 1)
513 NSString *name = saverName;
514 NSString *year = [self makeDesc:saverName yearOnly:YES];
517 CGRect frame = [saverView frame];
521 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
522 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
523 CGSize tsize1 = [name sizeWithFont:font1
524 constrainedToSize:CGSizeMake(frame.size.width,
526 CGSize tsize2 = [year sizeWithFont:font2
527 constrainedToSize:CGSizeMake(frame.size.width,
529 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
530 tsize1.width : tsize2.width,
531 tsize1.height + tsize2.height);
533 // Don't know how to find inner margin of UITextView.
535 tsize.width += margin * 4;
536 tsize.height += margin * 2;
538 if ([saverView frame].size.width >= 768)
539 tsize.height += pt1 * 3; // extra bottom margin on iPad
541 frame = CGRectMake (0, 0, tsize.width, tsize.height);
543 UIInterfaceOrientation orient =
544 // Why are both of these wrong when starting up rotated??
545 [[UIDevice currentDevice] orientation];
546 // [rootViewController interfaceOrientation];
548 /* Get the text oriented properly, and move it to the bottom of the
549 screen, since many savers have action in the middle.
552 case UIDeviceOrientationLandscapeRight:
554 frame.origin.x = ([saverView frame].size.width
555 - (tsize.width - tsize.height) / 2
557 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
559 case UIDeviceOrientationLandscapeLeft:
561 frame.origin.x = -(tsize.width - tsize.height) / 2;
562 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
564 case UIDeviceOrientationPortraitUpsideDown:
566 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
571 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
572 frame.origin.y = [saverView frame].size.height - tsize.height;
577 [aboutBox removeFromSuperview];
579 aboutBox = [[UIView alloc] initWithFrame:frame];
581 aboutBox.transform = CGAffineTransformMakeRotation (rot);
582 aboutBox.backgroundColor = [UIColor clearColor];
584 /* There seems to be no easy way to stroke the font, so instead draw
585 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
586 a black shadow to each. (You'd think the shadow alone would be
587 enough, but there's no way to make it dark enough to be legible.)
589 for (int i = 0; i < 5; i++) {
590 UITextView *textview;
592 frame.origin.x = frame.origin.y = 0;
594 case 0: frame.origin.x = -off; break;
595 case 1: frame.origin.x = off; break;
596 case 2: frame.origin.y = -off; break;
597 case 3: frame.origin.y = off; break;
600 for (int j = 0; j < 2; j++) {
602 frame.origin.y = (j == 0 ? 0 : pt1);
603 textview = [[UITextView alloc] initWithFrame:frame];
604 textview.font = (j == 0 ? font1 : font2);
605 textview.text = (j == 0 ? name : year);
606 textview.textAlignment = UITextAlignmentCenter;
607 textview.showsHorizontalScrollIndicator = NO;
608 textview.showsVerticalScrollIndicator = NO;
609 textview.scrollEnabled = NO;
610 textview.editable = NO;
611 textview.userInteractionEnabled = NO;
612 textview.backgroundColor = [UIColor clearColor];
613 textview.textColor = (i == 4
614 ? [UIColor yellowColor]
615 : [UIColor blackColor]);
617 CALayer *textLayer = (CALayer *)
618 [textview.layer.sublayers objectAtIndex:0];
619 textLayer.shadowColor = [UIColor blackColor].CGColor;
620 textLayer.shadowOffset = CGSizeMake(0, 0);
621 textLayer.shadowOpacity = 1;
622 textLayer.shadowRadius = 2;
624 [aboutBox addSubview:textview];
628 CABasicAnimation *anim =
629 [CABasicAnimation animationWithKeyPath:@"opacity"];
631 anim.repeatCount = 1;
632 anim.autoreverses = NO;
633 anim.fromValue = [NSNumber numberWithFloat:0.0];
634 anim.toValue = [NSNumber numberWithFloat:1.0];
635 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
637 [backgroundView addSubview:aboutBox];
640 [splashTimer invalidate];
643 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
645 selector:@selector(aboutOff)
648 # endif // USE_IPHONE
657 [splashTimer invalidate];
660 CABasicAnimation *anim =
661 [CABasicAnimation animationWithKeyPath:@"opacity"];
663 anim.repeatCount = 1;
664 anim.autoreverses = NO;
665 anim.fromValue = [NSNumber numberWithFloat: 1];
666 anim.toValue = [NSNumber numberWithFloat: 0];
667 anim.delegate = self;
668 aboutBox.layer.opacity = 0;
669 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
676 - (void)selectedSaverDidChange:(NSDictionary *)change
678 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
679 NSString *name = [prefs stringForKey:@"selectedSaverName"];
683 if (! [saverNames containsObject:name]) {
684 NSLog (@"saver \"%@\" does not exist", name);
688 [self loadSaver: name];
692 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
695 NSString *ext = @"saver";
697 NSString *ext = @"xml";
700 NSArray *files = [[NSFileManager defaultManager]
701 contentsOfDirectoryAtPath:dir error:nil];
702 if (! files) return 0;
703 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
705 for (NSString *p in files) {
706 if ([[p pathExtension] caseInsensitiveCompare: ext])
709 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
712 // Get the saver name's capitalization right by reading the XML file.
714 p = [dir stringByAppendingPathComponent: p];
715 NSData *xmld = [NSData dataWithContentsOfFile:p];
716 NSAssert (xmld, @"no XML: %@", p);
717 NSString *xml = [XScreenSaverView decompressXML:xmld];
718 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
719 NSAssert1 (r.length, @"no name in %@", p);
721 xml = [xml substringFromIndex: r.location + r.length];
722 r = [xml rangeOfString:@"\"" options:0];
723 if (r.length) name = [xml substringToIndex: r.location];
726 # endif // USE_IPHONE
728 NSAssert1 (name, @"no name in %@", p);
729 if (name) [result addObject: name];
732 if (! [result count])
740 - (NSArray *) listSaverBundleNames
742 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
745 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
746 // directories in the bundle.
747 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
748 stringByAppendingPathComponent:@"Contents"]
749 stringByAppendingPathComponent:@"Resources"]];
750 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
752 // Also look in the same directory as the executable.
753 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
754 stringByDeletingLastPathComponent]];
756 // Finally, look in standard MacOS screensaver directories.
757 // [dirs addObject: @"~/Library/Screen Savers"];
758 // [dirs addObject: @"/Library/Screen Savers"];
759 // [dirs addObject: @"/System/Library/Screen Savers"];
763 // On iOS, only look in the bundle's root directory.
764 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
766 # endif // USE_IPHONE
769 for (i = 0; i < [dirs count]; i++) {
770 NSString *dir = [dirs objectAtIndex:i];
771 NSArray *names = [self listSaverBundleNamesInDir:dir];
772 if (! names) continue;
773 saverDir = [dir retain];
774 saverNames = [names retain];
778 NSString *err = @"no .saver bundles found in: ";
779 for (i = 0; i < [dirs count]; i++) {
780 if (i) err = [err stringByAppendingString:@", "];
781 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
782 stringByAbbreviatingWithTildeInPath]];
783 err = [err stringByAppendingString:@"/"];
786 return [NSArray array];
790 /* Create the popup menu of available saver names.
794 - (NSPopUpButton *) makeMenu
797 rect.origin.x = rect.origin.y = 0;
798 rect.size.width = 10;
799 rect.size.height = 10;
800 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
804 for (i = 0; i < [saverNames count]; i++) {
805 NSString *name = [saverNames objectAtIndex:i];
806 [popup addItemWithTitle:name];
807 [[popup itemWithTitle:name] setRepresentedObject:name];
809 NSRect r = [popup frame];
810 if (r.size.width > max_width) max_width = r.size.width;
813 // Bind the menu to preferences, and trigger a callback when an item
816 NSString *key = @"values.selectedSaverName";
817 NSUserDefaultsController *prefs =
818 [NSUserDefaultsController sharedUserDefaultsController];
819 [prefs addObserver:self
822 context:@selector(selectedSaverDidChange:)];
823 [popup bind:@"selectedObject"
827 [prefs setAppliesImmediately:YES];
829 NSRect r = [popup frame];
830 r.size.width = max_width;
837 - (NSString *) makeDesc:(NSString *)saver
838 yearOnly:(BOOL) yearp
841 NSString *path = [saverDir stringByAppendingPathComponent:
842 [[saver lowercaseString]
843 stringByReplacingOccurrencesOfString:@" "
847 path = [path stringByAppendingPathExtension:@"xml"];
848 NSData *xmld = [NSData dataWithContentsOfFile:path];
849 if (! xmld) goto FAIL;
850 desc = [XScreenSaverView decompressXML:xmld];
851 if (! desc) goto FAIL;
853 r = [desc rangeOfString:@"<_description>"
854 options:NSCaseInsensitiveSearch];
859 desc = [desc substringFromIndex: r.location + r.length];
860 r = [desc rangeOfString:@"</_description>"
861 options:NSCaseInsensitiveSearch];
863 desc = [desc substringToIndex: r.location];
865 // Leading and trailing whitespace.
866 desc = [desc stringByTrimmingCharactersInSet:
867 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
869 // Let's see if we can find a year on the last line.
870 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
872 for (NSString *word in
873 [[desc substringFromIndex:r.location + r.length]
874 componentsSeparatedByCharactersInSet:
875 [NSCharacterSet characterSetWithCharactersInString:
877 int n = [word doubleValue];
878 if (n > 1970 && n < 2100)
882 // Delete everything after the first blank line.
883 r = [desc rangeOfString:@"\n\n" options:0];
885 desc = [desc substringToIndex: r.location];
887 // Truncate really long ones.
889 if ([desc length] > max)
890 desc = [desc substringToIndex: max];
893 desc = [year stringByAppendingString:
894 [@": " stringByAppendingString: desc]];
897 desc = year ? year : @"";
901 if ([saverNames count] > 1)
902 desc = @"Oops, this module appears to be incomplete.";
910 - (NSString *) makeDesc:(NSString *)saver
912 return [self makeDesc:saver yearOnly:NO];
917 /* Create a dictionary of one-line descriptions of every saver,
918 for display on the UITableView.
920 - (NSDictionary *)makeDescTable
922 NSMutableDictionary *dict =
923 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
924 for (NSString *saver in saverNames) {
925 [dict setObject:[self makeDesc:saver] forKey:saver];
935 /* This is called when the "selectedSaverName" pref changes, e.g.,
936 when a menu selection is made.
938 - (void)observeValueForKeyPath:(NSString *)keyPath
940 change:(NSDictionary *)change
941 context:(void *)context
943 SEL dispatchSelector = (SEL)context;
944 if (dispatchSelector != NULL) {
945 [self performSelector:dispatchSelector withObject:change];
947 [super observeValueForKeyPath:keyPath
957 /* Create the desktop window shell, possibly including a preferences button.
959 - (NSWindow *) makeWindow
962 static int count = 0;
963 Bool simple_p = ([saverNames count] == 1);
965 NSPopUpButton *menu = 0;
970 sv_rect.origin.x = sv_rect.origin.y = 0;
971 sv_rect.size.width = 320;
972 sv_rect.size.height = 240;
973 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
974 initWithFrame:sv_rect
977 // make a "Preferences" button
982 rect.size.width = rect.size.height = 10;
983 pb = [[NSButton alloc] initWithFrame:rect];
984 [pb setTitle:@"Preferences"];
985 [pb setBezelStyle:NSRoundedBezelStyle];
988 rect.origin.x = ([sv frame].size.width -
989 [pb frame].size.width) / 2;
990 [pb setFrameOrigin:rect.origin];
995 [pb setAction:@selector(openPreferences:)];
997 // Make a saver selection menu
999 menu = [self makeMenu];
1002 [menu setFrameOrigin:rect.origin];
1004 // make a box to wrap the saverView
1008 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1009 gbox = [[NSBox alloc] initWithFrame:rect];
1010 rect.size.width = rect.size.height = 10;
1011 [gbox setContentViewMargins:rect.size];
1012 [gbox setTitlePosition:NSNoTitle];
1013 [gbox addSubview:sv];
1016 // make a box to wrap the other two boxes
1018 rect.origin.x = rect.origin.y = 0;
1019 rect.size.width = [gbox frame].size.width;
1020 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1021 pbox = [[NSBox alloc] initWithFrame:rect];
1022 [pbox setTitlePosition:NSNoTitle];
1023 [pbox setBorderType:NSNoBorder];
1024 [pbox addSubview:gbox];
1025 if (menu) [pbox addSubview:menu];
1026 if (pb) [pbox addSubview:pb];
1029 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1030 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1031 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1032 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1035 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1038 // and make a window to hold that.
1040 NSScreen *screen = [NSScreen mainScreen];
1041 rect = pbox ? [pbox frame] : [sv frame];
1042 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1043 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1045 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1047 NSWindow *win = [[NSWindow alloc]
1048 initWithContentRect:rect
1049 styleMask:(NSTitledWindowMask |
1050 NSClosableWindowMask |
1051 NSMiniaturizableWindowMask |
1052 NSResizableWindowMask)
1053 backing:NSBackingStoreBuffered
1056 [win setMinSize:[win frameRectForContentRect:rect].size];
1057 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1059 [win makeKeyAndOrderFront:win];
1061 [sv startAnimation]; // this is the dummy saver
1071 for (NSWindow *win in windows) {
1072 ScreenSaverView *sv = find_saverView ([win contentView]);
1073 if ([sv isAnimating])
1074 [sv animateOneFrame];
1078 # endif // !USE_IPHONE
1081 - (void)applicationDidFinishLaunching:
1083 (NSNotification *) notif
1084 # else // USE_IPHONE
1085 (UIApplication *) application
1086 # endif // USE_IPHONE
1088 [self listSaverBundleNames];
1091 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1092 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1097 // Create either one window (for standalone, e.g. Phosphor.app)
1098 // or two windows for SaverTester.app.
1099 for (i = 0; i < window_count; i++) {
1100 NSWindow *win = [self makeWindow];
1101 // Get the last-saved window position out of preferences.
1102 [win setFrameAutosaveName:
1103 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1104 [win setFrameUsingName:[win frameAutosaveName]];
1106 // This prevents clicks from being seen by savers.
1107 // [win setMovableByWindowBackground:YES];
1109 # else // USE_IPHONE
1111 # undef ya_rand_init
1112 ya_rand_init (0); // Now's a good time.
1114 rootViewController = [[[RotateyViewController alloc] init] retain];
1115 [window setRootViewController: rootViewController];
1117 SaverListController *menu = [[SaverListController alloc]
1118 initWithNames:saverNames
1119 descriptions:[self makeDescTable]];
1120 [rootViewController pushViewController:menu animated:YES];
1121 [menu becomeFirstResponder];
1123 [window makeKeyAndVisible];
1124 [window setAutoresizesSubviews:YES];
1125 [window setAutoresizingMask:
1126 (UIViewAutoresizingFlexibleWidth |
1127 UIViewAutoresizingFlexibleHeight)];
1129 application.applicationSupportsShakeToEdit = YES;
1131 # endif // USE_IPHONE
1133 NSString *forced = 0;
1134 /* In the XCode project, each .saver scheme sets this env var when
1135 launching SaverTester.app so that it knows which one we are
1136 currently debugging. If this is set, it overrides the default
1137 selection in the popup menu. If unset, that menu persists to
1138 whatever it was last time.
1140 const char *f = getenv ("SELECTED_SAVER");
1142 forced = [NSString stringWithCString:(char *)f
1143 encoding:NSUTF8StringEncoding];
1145 if (forced && ![saverNames containsObject:forced]) {
1146 NSLog(@"forced saver \"%@\" does not exist", forced);
1150 // If there's only one saver, run that.
1151 if (!forced && [saverNames count] == 1)
1152 forced = [saverNames objectAtIndex:0];
1154 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1157 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1162 // If nothing was selected (e.g., this is the first launch)
1163 // then scroll randomly instead of starting up at "A".
1166 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1169 [menu scrollTo: prev];
1170 # endif // USE_IPHONE
1173 [prefs setObject:forced forKey:@"selectedSaverName"];
1176 /* Don't auto-launch the saver unless it was running last time.
1177 XScreenSaverView manages this, on crash_timer.
1180 if (!forced && ![prefs boolForKey:@"wasRunning"])
1184 [self selectedSaverDidChange:nil];
1188 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1189 ScreenSaverView to run its own timer calling animateOneFrame.
1190 On 10.9, that fails because the private class ScreenSaverModule
1191 is only initialized properly by ScreenSaverEngine, and in the
1192 context of SaverRunner, the null ScreenSaverEngine instance
1193 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1194 So, if it looks like this is the 10.9 version of ScreenSaverModule
1195 instead of the 10.8 version, we run our own timer here. This sucks.
1198 Class ssm = NSClassFromString (@"ScreenSaverModule");
1199 if (ssm && [ssm instancesRespondToSelector:
1200 @selector(needsAnimationTimer)]) {
1201 NSWindow *win = [windows objectAtIndex:0];
1202 ScreenSaverView *sv = find_saverView ([win contentView]);
1203 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1204 [sv animationTimeInterval]
1206 selector:@selector(animTimer)
1211 # endif // !USE_IPHONE
1217 /* When the window closes, exit (even if prefs still open.)
1219 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1224 # else // USE_IPHONE
1226 - (void)applicationWillResignActive:(UIApplication *)app
1228 [(XScreenSaverView *)view setScreenLocked:YES];
1231 - (void)applicationDidBecomeActive:(UIApplication *)app
1233 [(XScreenSaverView *)view setScreenLocked:NO];
1236 - (void)applicationDidEnterBackground:(UIApplication *)application
1238 [(XScreenSaverView *)view setScreenLocked:YES];
1241 #endif // USE_IPHONE