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];
448 [[NSNotificationCenter defaultCenter] removeObserver:saverView];
452 NSSize size = [window frame].size;
453 saverView = [self makeSaverView:name withSize: size];
456 [[[UIAlertView alloc] initWithTitle: name
457 message: @"Unable to load!"
459 cancelButtonTitle: @"Bummer"
460 otherButtonTitles: nil]
465 [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 if ([saverNames count] == 1)
514 NSString *name = saverName;
515 NSString *year = [self makeDesc:saverName yearOnly:YES];
518 CGRect frame = [saverView frame];
522 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
523 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
524 CGSize tsize1 = [name sizeWithFont:font1
525 constrainedToSize:CGSizeMake(frame.size.width,
527 CGSize tsize2 = [year sizeWithFont:font2
528 constrainedToSize:CGSizeMake(frame.size.width,
530 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
531 tsize1.width : tsize2.width,
532 tsize1.height + tsize2.height);
534 // Don't know how to find inner margin of UITextView.
536 tsize.width += margin * 4;
537 tsize.height += margin * 2;
539 if ([saverView frame].size.width >= 768)
540 tsize.height += pt1 * 3; // extra bottom margin on iPad
542 frame = CGRectMake (0, 0, tsize.width, tsize.height);
544 UIInterfaceOrientation orient =
545 // Why are both of these wrong when starting up rotated??
546 [[UIDevice currentDevice] orientation];
547 // [rootViewController interfaceOrientation];
549 /* Get the text oriented properly, and move it to the bottom of the
550 screen, since many savers have action in the middle.
553 case UIDeviceOrientationLandscapeRight:
555 frame.origin.x = ([saverView frame].size.width
556 - (tsize.width - tsize.height) / 2
558 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
560 case UIDeviceOrientationLandscapeLeft:
562 frame.origin.x = -(tsize.width - tsize.height) / 2;
563 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
565 case UIDeviceOrientationPortraitUpsideDown:
567 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
572 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
573 frame.origin.y = [saverView frame].size.height - tsize.height;
578 [aboutBox removeFromSuperview];
580 aboutBox = [[UIView alloc] initWithFrame:frame];
582 aboutBox.transform = CGAffineTransformMakeRotation (rot);
583 aboutBox.backgroundColor = [UIColor clearColor];
585 /* There seems to be no easy way to stroke the font, so instead draw
586 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
587 a black shadow to each. (You'd think the shadow alone would be
588 enough, but there's no way to make it dark enough to be legible.)
590 for (int i = 0; i < 5; i++) {
591 UITextView *textview;
593 frame.origin.x = frame.origin.y = 0;
595 case 0: frame.origin.x = -off; break;
596 case 1: frame.origin.x = off; break;
597 case 2: frame.origin.y = -off; break;
598 case 3: frame.origin.y = off; break;
601 for (int j = 0; j < 2; j++) {
603 frame.origin.y = (j == 0 ? 0 : pt1);
604 textview = [[UITextView alloc] initWithFrame:frame];
605 textview.font = (j == 0 ? font1 : font2);
606 textview.text = (j == 0 ? name : year);
607 textview.textAlignment = UITextAlignmentCenter;
608 textview.showsHorizontalScrollIndicator = NO;
609 textview.showsVerticalScrollIndicator = NO;
610 textview.scrollEnabled = NO;
611 textview.editable = NO;
612 textview.userInteractionEnabled = NO;
613 textview.backgroundColor = [UIColor clearColor];
614 textview.textColor = (i == 4
615 ? [UIColor yellowColor]
616 : [UIColor blackColor]);
618 CALayer *textLayer = (CALayer *)
619 [textview.layer.sublayers objectAtIndex:0];
620 textLayer.shadowColor = [UIColor blackColor].CGColor;
621 textLayer.shadowOffset = CGSizeMake(0, 0);
622 textLayer.shadowOpacity = 1;
623 textLayer.shadowRadius = 2;
625 [aboutBox addSubview:textview];
629 CABasicAnimation *anim =
630 [CABasicAnimation animationWithKeyPath:@"opacity"];
632 anim.repeatCount = 1;
633 anim.autoreverses = NO;
634 anim.fromValue = [NSNumber numberWithFloat:0.0];
635 anim.toValue = [NSNumber numberWithFloat:1.0];
636 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
638 [backgroundView addSubview:aboutBox];
641 [splashTimer invalidate];
644 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
646 selector:@selector(aboutOff)
649 # endif // USE_IPHONE
658 [splashTimer invalidate];
661 CABasicAnimation *anim =
662 [CABasicAnimation animationWithKeyPath:@"opacity"];
664 anim.repeatCount = 1;
665 anim.autoreverses = NO;
666 anim.fromValue = [NSNumber numberWithFloat: 1];
667 anim.toValue = [NSNumber numberWithFloat: 0];
668 anim.delegate = self;
669 aboutBox.layer.opacity = 0;
670 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
677 - (void)selectedSaverDidChange:(NSDictionary *)change
679 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
680 NSString *name = [prefs stringForKey:@"selectedSaverName"];
684 if (! [saverNames containsObject:name]) {
685 NSLog (@"saver \"%@\" does not exist", name);
689 [self loadSaver: name];
693 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
696 NSString *ext = @"saver";
698 NSString *ext = @"xml";
701 NSArray *files = [[NSFileManager defaultManager]
702 contentsOfDirectoryAtPath:dir error:nil];
703 if (! files) return 0;
704 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
706 for (NSString *p in files) {
707 if ([[p pathExtension] caseInsensitiveCompare: ext])
710 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
713 // Get the saver name's capitalization right by reading the XML file.
715 p = [dir stringByAppendingPathComponent: p];
716 NSData *xmld = [NSData dataWithContentsOfFile:p];
717 NSAssert (xmld, @"no XML: %@", p);
718 NSString *xml = [XScreenSaverView decompressXML:xmld];
719 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
720 NSAssert1 (r.length, @"no name in %@", p);
722 xml = [xml substringFromIndex: r.location + r.length];
723 r = [xml rangeOfString:@"\"" options:0];
724 if (r.length) name = [xml substringToIndex: r.location];
727 # endif // USE_IPHONE
729 NSAssert1 (name, @"no name in %@", p);
730 if (name) [result addObject: name];
733 if (! [result count])
741 - (NSArray *) listSaverBundleNames
743 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
746 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
747 // directories in the bundle.
748 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
749 stringByAppendingPathComponent:@"Contents"]
750 stringByAppendingPathComponent:@"Resources"]];
751 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
753 // Also look in the same directory as the executable.
754 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
755 stringByDeletingLastPathComponent]];
757 // Finally, look in standard MacOS screensaver directories.
758 // [dirs addObject: @"~/Library/Screen Savers"];
759 // [dirs addObject: @"/Library/Screen Savers"];
760 // [dirs addObject: @"/System/Library/Screen Savers"];
764 // On iOS, only look in the bundle's root directory.
765 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
767 # endif // USE_IPHONE
770 for (i = 0; i < [dirs count]; i++) {
771 NSString *dir = [dirs objectAtIndex:i];
772 NSArray *names = [self listSaverBundleNamesInDir:dir];
773 if (! names) continue;
774 saverDir = [dir retain];
775 saverNames = [names retain];
779 NSString *err = @"no .saver bundles found in: ";
780 for (i = 0; i < [dirs count]; i++) {
781 if (i) err = [err stringByAppendingString:@", "];
782 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
783 stringByAbbreviatingWithTildeInPath]];
784 err = [err stringByAppendingString:@"/"];
787 return [NSArray array];
791 /* Create the popup menu of available saver names.
795 - (NSPopUpButton *) makeMenu
798 rect.origin.x = rect.origin.y = 0;
799 rect.size.width = 10;
800 rect.size.height = 10;
801 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
805 for (i = 0; i < [saverNames count]; i++) {
806 NSString *name = [saverNames objectAtIndex:i];
807 [popup addItemWithTitle:name];
808 [[popup itemWithTitle:name] setRepresentedObject:name];
810 NSRect r = [popup frame];
811 if (r.size.width > max_width) max_width = r.size.width;
814 // Bind the menu to preferences, and trigger a callback when an item
817 NSString *key = @"values.selectedSaverName";
818 NSUserDefaultsController *prefs =
819 [NSUserDefaultsController sharedUserDefaultsController];
820 [prefs addObserver:self
823 context:@selector(selectedSaverDidChange:)];
824 [popup bind:@"selectedObject"
828 [prefs setAppliesImmediately:YES];
830 NSRect r = [popup frame];
831 r.size.width = max_width;
838 - (NSString *) makeDesc:(NSString *)saver
839 yearOnly:(BOOL) yearp
842 NSString *path = [saverDir stringByAppendingPathComponent:
843 [[saver lowercaseString]
844 stringByReplacingOccurrencesOfString:@" "
848 path = [path stringByAppendingPathExtension:@"xml"];
849 NSData *xmld = [NSData dataWithContentsOfFile:path];
850 if (! xmld) goto FAIL;
851 desc = [XScreenSaverView decompressXML:xmld];
852 if (! desc) goto FAIL;
854 r = [desc rangeOfString:@"<_description>"
855 options:NSCaseInsensitiveSearch];
860 desc = [desc substringFromIndex: r.location + r.length];
861 r = [desc rangeOfString:@"</_description>"
862 options:NSCaseInsensitiveSearch];
864 desc = [desc substringToIndex: r.location];
866 // Leading and trailing whitespace.
867 desc = [desc stringByTrimmingCharactersInSet:
868 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
870 // Let's see if we can find a year on the last line.
871 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
873 for (NSString *word in
874 [[desc substringFromIndex:r.location + r.length]
875 componentsSeparatedByCharactersInSet:
876 [NSCharacterSet characterSetWithCharactersInString:
878 int n = [word doubleValue];
879 if (n > 1970 && n < 2100)
883 // Delete everything after the first blank line.
884 r = [desc rangeOfString:@"\n\n" options:0];
886 desc = [desc substringToIndex: r.location];
888 // Truncate really long ones.
890 if ([desc length] > max)
891 desc = [desc substringToIndex: max];
894 desc = [year stringByAppendingString:
895 [@": " stringByAppendingString: desc]];
898 desc = year ? year : @"";
902 if ([saverNames count] > 1)
903 desc = @"Oops, this module appears to be incomplete.";
911 - (NSString *) makeDesc:(NSString *)saver
913 return [self makeDesc:saver yearOnly:NO];
918 /* Create a dictionary of one-line descriptions of every saver,
919 for display on the UITableView.
921 - (NSDictionary *)makeDescTable
923 NSMutableDictionary *dict =
924 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
925 for (NSString *saver in saverNames) {
926 [dict setObject:[self makeDesc:saver] forKey:saver];
936 /* This is called when the "selectedSaverName" pref changes, e.g.,
937 when a menu selection is made.
939 - (void)observeValueForKeyPath:(NSString *)keyPath
941 change:(NSDictionary *)change
942 context:(void *)context
944 SEL dispatchSelector = (SEL)context;
945 if (dispatchSelector != NULL) {
946 [self performSelector:dispatchSelector withObject:change];
948 [super observeValueForKeyPath:keyPath
958 /* Create the desktop window shell, possibly including a preferences button.
960 - (NSWindow *) makeWindow
963 static int count = 0;
964 Bool simple_p = ([saverNames count] == 1);
966 NSPopUpButton *menu = 0;
971 sv_rect.origin.x = sv_rect.origin.y = 0;
972 sv_rect.size.width = 320;
973 sv_rect.size.height = 240;
974 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
975 initWithFrame:sv_rect
978 // make a "Preferences" button
983 rect.size.width = rect.size.height = 10;
984 pb = [[NSButton alloc] initWithFrame:rect];
985 [pb setTitle:@"Preferences"];
986 [pb setBezelStyle:NSRoundedBezelStyle];
989 rect.origin.x = ([sv frame].size.width -
990 [pb frame].size.width) / 2;
991 [pb setFrameOrigin:rect.origin];
996 [pb setAction:@selector(openPreferences:)];
998 // Make a saver selection menu
1000 menu = [self makeMenu];
1003 [menu setFrameOrigin:rect.origin];
1005 // make a box to wrap the saverView
1009 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1010 gbox = [[NSBox alloc] initWithFrame:rect];
1011 rect.size.width = rect.size.height = 10;
1012 [gbox setContentViewMargins:rect.size];
1013 [gbox setTitlePosition:NSNoTitle];
1014 [gbox addSubview:sv];
1017 // make a box to wrap the other two boxes
1019 rect.origin.x = rect.origin.y = 0;
1020 rect.size.width = [gbox frame].size.width;
1021 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1022 pbox = [[NSBox alloc] initWithFrame:rect];
1023 [pbox setTitlePosition:NSNoTitle];
1024 [pbox setBorderType:NSNoBorder];
1025 [pbox addSubview:gbox];
1026 if (menu) [pbox addSubview:menu];
1027 if (pb) [pbox addSubview:pb];
1030 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1031 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1032 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1033 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1036 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1039 // and make a window to hold that.
1041 NSScreen *screen = [NSScreen mainScreen];
1042 rect = pbox ? [pbox frame] : [sv frame];
1043 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1044 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1046 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1048 NSWindow *win = [[NSWindow alloc]
1049 initWithContentRect:rect
1050 styleMask:(NSTitledWindowMask |
1051 NSClosableWindowMask |
1052 NSMiniaturizableWindowMask |
1053 NSResizableWindowMask)
1054 backing:NSBackingStoreBuffered
1057 [win setMinSize:[win frameRectForContentRect:rect].size];
1058 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1060 [win makeKeyAndOrderFront:win];
1062 [sv startAnimation]; // this is the dummy saver
1072 for (NSWindow *win in windows) {
1073 ScreenSaverView *sv = find_saverView ([win contentView]);
1074 if ([sv isAnimating])
1075 [sv animateOneFrame];
1079 # endif // !USE_IPHONE
1082 - (void)applicationDidFinishLaunching:
1084 (NSNotification *) notif
1085 # else // USE_IPHONE
1086 (UIApplication *) application
1087 # endif // USE_IPHONE
1089 [self listSaverBundleNames];
1092 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1093 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1098 // Create either one window (for standalone, e.g. Phosphor.app)
1099 // or two windows for SaverTester.app.
1100 for (i = 0; i < window_count; i++) {
1101 NSWindow *win = [self makeWindow];
1102 // Get the last-saved window position out of preferences.
1103 [win setFrameAutosaveName:
1104 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1105 [win setFrameUsingName:[win frameAutosaveName]];
1107 // This prevents clicks from being seen by savers.
1108 // [win setMovableByWindowBackground:YES];
1110 # else // USE_IPHONE
1112 # undef ya_rand_init
1113 ya_rand_init (0); // Now's a good time.
1115 rootViewController = [[[RotateyViewController alloc] init] retain];
1116 [window setRootViewController: rootViewController];
1118 SaverListController *menu = [[SaverListController alloc]
1119 initWithNames:saverNames
1120 descriptions:[self makeDescTable]];
1121 [rootViewController pushViewController:menu animated:YES];
1122 [menu becomeFirstResponder];
1124 [window makeKeyAndVisible];
1125 [window setAutoresizesSubviews:YES];
1126 [window setAutoresizingMask:
1127 (UIViewAutoresizingFlexibleWidth |
1128 UIViewAutoresizingFlexibleHeight)];
1130 application.applicationSupportsShakeToEdit = YES;
1132 # endif // USE_IPHONE
1134 NSString *forced = 0;
1135 /* In the XCode project, each .saver scheme sets this env var when
1136 launching SaverTester.app so that it knows which one we are
1137 currently debugging. If this is set, it overrides the default
1138 selection in the popup menu. If unset, that menu persists to
1139 whatever it was last time.
1141 const char *f = getenv ("SELECTED_SAVER");
1143 forced = [NSString stringWithCString:(char *)f
1144 encoding:NSUTF8StringEncoding];
1146 if (forced && ![saverNames containsObject:forced]) {
1147 NSLog(@"forced saver \"%@\" does not exist", forced);
1151 // If there's only one saver, run that.
1152 if (!forced && [saverNames count] == 1)
1153 forced = [saverNames objectAtIndex:0];
1155 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1158 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1163 // If nothing was selected (e.g., this is the first launch)
1164 // then scroll randomly instead of starting up at "A".
1167 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1170 [menu scrollTo: prev];
1171 # endif // USE_IPHONE
1174 [prefs setObject:forced forKey:@"selectedSaverName"];
1177 /* Don't auto-launch the saver unless it was running last time.
1178 XScreenSaverView manages this, on crash_timer.
1181 if (!forced && ![prefs boolForKey:@"wasRunning"])
1185 [self selectedSaverDidChange:nil];
1189 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1190 ScreenSaverView to run its own timer calling animateOneFrame.
1191 On 10.9, that fails because the private class ScreenSaverModule
1192 is only initialized properly by ScreenSaverEngine, and in the
1193 context of SaverRunner, the null ScreenSaverEngine instance
1194 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1195 So, if it looks like this is the 10.9 version of ScreenSaverModule
1196 instead of the 10.8 version, we run our own timer here. This sucks.
1199 Class ssm = NSClassFromString (@"ScreenSaverModule");
1200 if (ssm && [ssm instancesRespondToSelector:
1201 @selector(needsAnimationTimer)]) {
1202 NSWindow *win = [windows objectAtIndex:0];
1203 ScreenSaverView *sv = find_saverView ([win contentView]);
1204 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1205 [sv animationTimeInterval]
1207 selector:@selector(animTimer)
1212 # endif // !USE_IPHONE
1218 /* When the window closes, exit (even if prefs still open.)
1220 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1225 # else // USE_IPHONE
1227 - (void)applicationWillResignActive:(UIApplication *)app
1229 [(XScreenSaverView *)view setScreenLocked:YES];
1232 - (void)applicationDidBecomeActive:(UIApplication *)app
1234 [(XScreenSaverView *)view setScreenLocked:NO];
1237 - (void)applicationDidEnterBackground:(UIApplication *)application
1239 [(XScreenSaverView *)view setScreenLocked:YES];
1242 #endif // USE_IPHONE