1 /* xscreensaver, Copyright (c) 2006-2014 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
46 @implementation RotateyViewController
48 /* This subclass exists so that we can ask that the SaverListController and
49 preferences panels be auto-rotated by the system. Note that the
50 XScreenSaverView is not auto-rotated because it is on a different UIWindow.
53 - (id)initWithRotation:(BOOL)rotatep
56 allowRotation = rotatep;
60 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
62 return allowRotation; /* Deprecated in iOS 6 */
65 - (BOOL)shouldAutorotate /* Added in iOS 6 */
70 - (NSUInteger)supportedInterfaceOrientations /* Added in iOS 6 */
72 return UIInterfaceOrientationMaskAll;
80 @implementation SaverRunner
83 - (ScreenSaverView *) makeSaverView: (NSString *) module
84 withSize: (NSSize) size
90 // Load the XScreenSaverView subclass and code from a ".saver" bundle.
92 NSString *name = [module stringByAppendingPathExtension:@"saver"];
93 NSString *path = [saverDir stringByAppendingPathComponent:name];
95 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
96 NSLog(@"bundle \"%@\" does not exist", path);
100 NSLog(@"Loading %@", path);
102 // NSBundle *obundle = saverBundle;
104 saverBundle = [NSBundle bundleWithPath:path];
106 new_class = [saverBundle principalClass];
108 // Not entirely unsurprisingly, this tends to break the world.
109 // if (obundle && obundle != saverBundle)
114 // Determine whether to create an X11 view or an OpenGL view by
115 // looking for the "gl" tag in the xml file. This is kind of awful.
117 NSString *path = [saverDir
118 stringByAppendingPathComponent:
119 [[[module lowercaseString]
120 stringByReplacingOccurrencesOfString:@" "
122 stringByAppendingPathExtension:@"xml"]];
123 NSData *xmld = [NSData dataWithContentsOfFile:path];
124 NSAssert (xmld, @"no XML: %@", path);
125 NSString *xml = [XScreenSaverView decompressXML:xmld];
126 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
129 ? [XScreenSaverGLView class]
130 : [XScreenSaverView class]);
132 # endif // USE_IPHONE
138 rect.origin.x = rect.origin.y = 0;
139 rect.size.width = size.width;
140 rect.size.height = size.height;
142 XScreenSaverView *instance =
143 [(XScreenSaverView *) [new_class alloc]
148 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
153 /* KLUGE: Inform the underlying program that we're in "standalone"
154 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
155 This is kind of horrible but I haven't thought of a more sensible
156 way to make this work.
159 if ([saverNames count] == 1) {
160 putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
164 return (ScreenSaverView *) instance;
170 static ScreenSaverView *
171 find_saverView_child (NSView *v)
173 NSArray *kids = [v subviews];
174 int nkids = [kids count];
176 for (i = 0; i < nkids; i++) {
177 NSObject *kid = [kids objectAtIndex:i];
178 if ([kid isKindOfClass:[ScreenSaverView class]]) {
179 return (ScreenSaverView *) kid;
181 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
189 static ScreenSaverView *
190 find_saverView (NSView *v)
193 NSView *p = [v superview];
197 return find_saverView_child (v);
201 /* Changes the contents of the menubar menus to correspond to
202 the running saver. Desktop only.
205 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
207 if ([v isKindOfClass:[NSMenu class]]) {
208 NSMenu *m = (NSMenu *)v;
209 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
210 withString:new_str]];
211 NSArray *kids = [m itemArray];
212 int nkids = [kids count];
214 for (i = 0; i < nkids; i++) {
215 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
217 } else if ([v isKindOfClass:[NSMenuItem class]]) {
218 NSMenuItem *mi = (NSMenuItem *)v;
219 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
220 withString:new_str]];
221 NSMenu *m = [mi submenu];
222 if (m) relabel_menus (m, old_str, new_str);
227 - (void) openPreferences: (id) sender
230 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
231 sv = find_saverView ((NSView *) sender);
235 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
236 w = [windows objectAtIndex:i];
237 if ([w isKeyWindow]) break;
239 sv = find_saverView ([w contentView]);
242 NSAssert (sv, @"no saver view");
244 NSWindow *prefs = [sv configureSheet];
246 [NSApp beginSheet:prefs
247 modalForWindow:[sv window]
249 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
251 int code = [NSApp runModalForWindow:prefs];
253 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
254 We have to restart *both* animations, because the xlockmore-style
255 ones will blow up if one re-inits but the other doesn't.
257 if (code != NSCancelButton) {
258 if ([sv isAnimating])
265 - (void) preferencesClosed: (NSWindow *) sheet
266 returnCode: (int) returnCode
267 contextInfo: (void *) contextInfo
269 [NSApp stopModalWithCode:returnCode];
275 - (UIImage *) screenshot
277 return saved_screenshot;
280 - (void) saveScreenshot
282 // Most of this is from:
283 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
284 // The rotation stuff is by me.
286 CGSize size = [[UIScreen mainScreen] bounds].size;
288 UIInterfaceOrientation orient =
289 [[window rootViewController] interfaceOrientation];
290 if (orient == UIInterfaceOrientationLandscapeLeft ||
291 orient == UIInterfaceOrientationLandscapeRight) {
292 // Rotate the shape of the canvas 90 degrees.
293 double s = size.width;
294 size.width = size.height;
299 // Create a graphics context with the target size
300 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
301 // take the scale into consideration
302 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
304 if (UIGraphicsBeginImageContextWithOptions)
305 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
307 UIGraphicsBeginImageContext (size);
309 CGContextRef ctx = UIGraphicsGetCurrentContext();
312 // Rotate the graphics context to match current hardware rotation.
315 case UIInterfaceOrientationPortraitUpsideDown:
316 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
317 CGContextRotateCTM (ctx, M_PI);
318 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
320 case UIInterfaceOrientationLandscapeLeft:
321 case UIInterfaceOrientationLandscapeRight:
322 CGContextTranslateCTM (ctx,
323 ([window frame].size.height -
324 [window frame].size.width) / 2,
325 ([window frame].size.width -
326 [window frame].size.height) / 2);
327 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
328 CGContextRotateCTM (ctx,
329 (orient == UIInterfaceOrientationLandscapeLeft
332 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
338 // Iterate over every window from back to front
340 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
341 if (![win respondsToSelector:@selector(screen)] ||
342 [win screen] == [UIScreen mainScreen]) {
344 // -renderInContext: renders in the coordinate space of the layer,
345 // so we must first apply the layer's geometry to the graphics context
346 CGContextSaveGState (ctx);
348 // Center the context around the window's anchor point
349 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
351 // Apply the window's transform about the anchor point
352 CGContextConcatCTM (ctx, [win transform]);
354 // Offset by the portion of the bounds left of and above anchor point
355 CGContextTranslateCTM (ctx,
356 -[win bounds].size.width * [[win layer] anchorPoint].x,
357 -[win bounds].size.height * [[win layer] anchorPoint].y);
359 // Render the layer hierarchy to the current context
360 [[win layer] renderInContext:ctx];
362 // Restore the context
363 CGContextRestoreGState (ctx);
367 if (saved_screenshot)
368 [saved_screenshot release];
369 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
371 UIGraphicsEndImageContext();
375 - (void) openPreferences: (NSString *) saver
377 [self loadSaver:saver launch:NO];
378 if (! saverView) return;
380 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
381 [prefs setObject:saver forKey:@"selectedSaverName"];
384 [rotating_nav pushViewController: [saverView configureView]
393 - (void)loadSaver:(NSString *)name launch:(BOOL)launch
397 if (saverName && [saverName isEqualToString: name]) {
399 for (NSWindow *win in windows) {
400 ScreenSaverView *sv = find_saverView ([win contentView]);
401 if (![sv isAnimating])
409 for (NSWindow *win in windows) {
410 NSView *cv = [win contentView];
411 NSString *old_title = [win title];
412 if (!old_title) old_title = @"XScreenSaver";
413 [win setTitle: name];
414 relabel_menus (menubar, old_title, name);
416 ScreenSaverView *old_view = find_saverView (cv);
417 NSView *sup = old_view ? [old_view superview] : cv;
420 if ([old_view isAnimating])
421 [old_view stopAnimation];
422 [old_view removeFromSuperview];
425 NSSize size = [cv frame].size;
426 ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
427 NSAssert (new_view, @"unable to make a saver view");
429 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
430 [sup addSubview: new_view];
431 [win makeFirstResponder:new_view];
432 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
435 [new_view startAnimation];
438 NSUserDefaultsController *ctl =
439 [NSUserDefaultsController sharedUserDefaultsController];
444 # if TARGET_IPHONE_SIMULATOR
445 NSLog (@"selecting saver \"%@\"", name);
448 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
449 [prefs setObject:name forKey:@"selectedSaverName"];
452 if (saverName && [saverName isEqualToString: name]) {
453 if ([saverView isAnimating])
462 if ([saverView isAnimating])
463 [saverView stopAnimation];
464 [saverView removeFromSuperview];
465 [backgroundView removeFromSuperview];
466 [[NSNotificationCenter defaultCenter] removeObserver:saverView];
470 /* We can't just use [window bounds] because that is the *rotated* rectangle
471 and we need the *unrotated* rectangle, so that the view is always created
472 in portrait orientation. Without that, the initial rotation event that
473 takes us from unknown->landscape will be out of step with reality.
475 UIScreen *screen = [UIScreen mainScreen];
476 # ifndef __IPHONE_8_0 // iOS 7 SDK
477 NSSize size = [screen bounds].size;
478 int ss = [screen scale];
480 NSSize size = ([screen respondsToSelector:@selector(nativeBounds)]
481 ? [screen nativeBounds].size // iOS 8
482 : [screen bounds].size); // iOS 7
483 int ss = ([screen respondsToSelector:@selector(nativeScale)]
484 ? [screen nativeScale] // iOS 8
485 : [screen scale]); // iOS 7
490 saverView = [self makeSaverView:name withSize:size];
493 [[[UIAlertView alloc] initWithTitle: name
494 message: @"Unable to load!"
496 cancelButtonTitle: @"Bummer"
497 otherButtonTitles: nil]
502 [[NSNotificationCenter defaultCenter]
503 addObserver:saverView
504 selector:@selector(didRotate:)
505 name:UIDeviceOrientationDidChangeNotification object:nil];
510 [self saveScreenshot];
511 NSRect f = [saverWindow bounds];
512 [backgroundView setFrame:f];
513 [saverView setFrame:f];
514 [saverWindow addSubview: backgroundView];
515 [backgroundView addSubview: saverView];
516 [saverView setBackgroundColor:[NSColor blackColor]];
518 /* WTF! Without creating and keying this window, we get no events
519 delivered on the saverView/saverWindow! Bad craziness.
522 UIWindow *dummy = [[UIWindow alloc] initWithFrame:CGRectMake(0,0,0,0)];
523 [dummy setRootViewController: nonrotating_nav]; // Must be this one.
524 [dummy setHidden:NO]; // required
525 [dummy setHidden:YES];
529 [saverWindow setHidden:NO];
530 [saverWindow makeKeyAndVisible];
531 [saverView startAnimation];
532 [self aboutPanel:nil];
534 // Doing this makes savers cut back to the list instead of fading,
535 // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
536 // [window setHidden:YES];
538 # endif // USE_IPHONE
542 - (void)loadSaver:(NSString *)name
544 [self loadSaver:name launch:YES];
548 - (void)aboutPanel:(id)sender
552 NSDictionary *bd = [saverBundle infoDictionary];
553 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
555 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
556 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
557 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
558 forKey:@"ApplicationVersion"];
559 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
560 [d setValue:[[NSAttributedString alloc]
561 initWithString: (NSString *)
562 [bd objectForKey:@"CFBundleGetInfoString"]]
565 [[NSApplication sharedApplication]
566 orderFrontStandardAboutPanelWithOptions:d];
569 if ([saverNames count] == 1)
572 NSString *name = saverName;
573 NSString *year = [self makeDesc:saverName yearOnly:YES];
576 CGRect frame = [saverView frame];
580 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
581 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
584 CGSize s = CGSizeMake(frame.size.width, frame.size.height);
585 CGSize tsize1 = [[[NSAttributedString alloc]
587 attributes:@{ NSFontAttributeName: font1 }]
588 boundingRectWithSize: s
589 options: NSStringDrawingUsesLineFragmentOrigin
591 CGSize tsize2 = [[[NSAttributedString alloc]
593 attributes:@{ NSFontAttributeName: font2 }]
594 boundingRectWithSize: s
595 options: NSStringDrawingUsesLineFragmentOrigin
597 # else // iOS 6 or Cocoa
598 CGSize tsize1 = [name sizeWithFont:font1
599 constrainedToSize:CGSizeMake(frame.size.width,
601 CGSize tsize2 = [year sizeWithFont:font2
602 constrainedToSize:CGSizeMake(frame.size.width,
606 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
607 tsize1.width : tsize2.width,
608 tsize1.height + tsize2.height);
610 tsize.width = ceilf(tsize.width);
611 tsize.height = ceilf(tsize.height);
613 // Don't know how to find inner margin of UITextView.
615 tsize.width += margin * 4;
616 tsize.height += margin * 2;
618 if ([saverView frame].size.width >= 768)
619 tsize.height += pt1 * 3; // extra bottom margin on iPad
621 frame = CGRectMake (0, 0, tsize.width, tsize.height);
623 UIInterfaceOrientation orient = [rotating_nav interfaceOrientation];
625 /* Get the text oriented properly, and move it to the bottom of the
626 screen, since many savers have action in the middle.
629 case UIDeviceOrientationLandscapeRight:
631 frame.origin.x = ([saverView frame].size.width
632 - (tsize.width - tsize.height) / 2
634 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
636 case UIDeviceOrientationLandscapeLeft:
638 frame.origin.x = -(tsize.width - tsize.height) / 2;
639 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
641 case UIDeviceOrientationPortraitUpsideDown:
643 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
648 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
649 frame.origin.y = [saverView frame].size.height - tsize.height;
654 [aboutBox removeFromSuperview];
656 aboutBox = [[UIView alloc] initWithFrame:frame];
658 aboutBox.transform = CGAffineTransformMakeRotation (rot);
659 aboutBox.backgroundColor = [UIColor clearColor];
661 /* There seems to be no easy way to stroke the font, so instead draw
662 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
663 a black shadow to each. (You'd think the shadow alone would be
664 enough, but there's no way to make it dark enough to be legible.)
666 for (int i = 0; i < 5; i++) {
667 UITextView *textview;
669 frame.origin.x = frame.origin.y = 0;
671 case 0: frame.origin.x = -off; break;
672 case 1: frame.origin.x = off; break;
673 case 2: frame.origin.y = -off; break;
674 case 3: frame.origin.y = off; break;
677 for (int j = 0; j < 2; j++) {
679 frame.origin.y = (j == 0 ? 0 : pt1);
680 textview = [[UITextView alloc] initWithFrame:frame];
681 textview.font = (j == 0 ? font1 : font2);
682 textview.text = (j == 0 ? name : year);
683 textview.textAlignment = NSTextAlignmentCenter;
684 textview.showsHorizontalScrollIndicator = NO;
685 textview.showsVerticalScrollIndicator = NO;
686 textview.scrollEnabled = NO;
687 textview.editable = NO;
688 textview.userInteractionEnabled = NO;
689 textview.backgroundColor = [UIColor clearColor];
690 textview.textColor = (i == 4
691 ? [UIColor yellowColor]
692 : [UIColor blackColor]);
694 CALayer *textLayer = (CALayer *)
695 [textview.layer.sublayers objectAtIndex:0];
696 textLayer.shadowColor = [UIColor blackColor].CGColor;
697 textLayer.shadowOffset = CGSizeMake(0, 0);
698 textLayer.shadowOpacity = 1;
699 textLayer.shadowRadius = 2;
701 [aboutBox addSubview:textview];
705 CABasicAnimation *anim =
706 [CABasicAnimation animationWithKeyPath:@"opacity"];
708 anim.repeatCount = 1;
709 anim.autoreverses = NO;
710 anim.fromValue = [NSNumber numberWithFloat:0.0];
711 anim.toValue = [NSNumber numberWithFloat:1.0];
712 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
714 [backgroundView addSubview:aboutBox];
717 [splashTimer invalidate];
720 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
722 selector:@selector(aboutOff)
725 # endif // USE_IPHONE
734 [splashTimer invalidate];
737 CABasicAnimation *anim =
738 [CABasicAnimation animationWithKeyPath:@"opacity"];
740 anim.repeatCount = 1;
741 anim.autoreverses = NO;
742 anim.fromValue = [NSNumber numberWithFloat: 1];
743 anim.toValue = [NSNumber numberWithFloat: 0];
744 anim.delegate = self;
745 aboutBox.layer.opacity = 0;
746 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
753 - (void)selectedSaverDidChange:(NSDictionary *)change
755 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
756 NSString *name = [prefs stringForKey:@"selectedSaverName"];
760 if (! [saverNames containsObject:name]) {
761 NSLog (@"saver \"%@\" does not exist", name);
765 [self loadSaver: name];
769 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
772 NSString *ext = @"saver";
774 NSString *ext = @"xml";
777 NSArray *files = [[NSFileManager defaultManager]
778 contentsOfDirectoryAtPath:dir error:nil];
779 if (! files) return 0;
780 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
782 for (NSString *p in files) {
783 if ([[p pathExtension] caseInsensitiveCompare: ext])
786 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
789 // Get the saver name's capitalization right by reading the XML file.
791 p = [dir stringByAppendingPathComponent: p];
792 NSData *xmld = [NSData dataWithContentsOfFile:p];
793 NSAssert (xmld, @"no XML: %@", p);
794 NSString *xml = [XScreenSaverView decompressXML:xmld];
795 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
796 NSAssert1 (r.length, @"no name in %@", p);
798 xml = [xml substringFromIndex: r.location + r.length];
799 r = [xml rangeOfString:@"\"" options:0];
800 if (r.length) name = [xml substringToIndex: r.location];
803 # endif // USE_IPHONE
805 NSAssert1 (name, @"no name in %@", p);
806 if (name) [result addObject: name];
809 if (! [result count])
817 - (NSArray *) listSaverBundleNames
819 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
822 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
823 // directories in the bundle.
824 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
825 stringByAppendingPathComponent:@"Contents"]
826 stringByAppendingPathComponent:@"Resources"]];
827 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
829 // Also look in the same directory as the executable.
830 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
831 stringByDeletingLastPathComponent]];
833 // Finally, look in standard MacOS screensaver directories.
834 // [dirs addObject: @"~/Library/Screen Savers"];
835 // [dirs addObject: @"/Library/Screen Savers"];
836 // [dirs addObject: @"/System/Library/Screen Savers"];
840 // On iOS, only look in the bundle's root directory.
841 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
843 # endif // USE_IPHONE
846 for (i = 0; i < [dirs count]; i++) {
847 NSString *dir = [dirs objectAtIndex:i];
848 NSArray *names = [self listSaverBundleNamesInDir:dir];
849 if (! names) continue;
850 saverDir = [dir retain];
851 saverNames = [names retain];
855 NSString *err = @"no .saver bundles found in: ";
856 for (i = 0; i < [dirs count]; i++) {
857 if (i) err = [err stringByAppendingString:@", "];
858 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
859 stringByAbbreviatingWithTildeInPath]];
860 err = [err stringByAppendingString:@"/"];
863 return [NSArray array];
867 /* Create the popup menu of available saver names.
871 - (NSPopUpButton *) makeMenu
874 rect.origin.x = rect.origin.y = 0;
875 rect.size.width = 10;
876 rect.size.height = 10;
877 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
881 for (i = 0; i < [saverNames count]; i++) {
882 NSString *name = [saverNames objectAtIndex:i];
883 [popup addItemWithTitle:name];
884 [[popup itemWithTitle:name] setRepresentedObject:name];
886 NSRect r = [popup frame];
887 if (r.size.width > max_width) max_width = r.size.width;
890 // Bind the menu to preferences, and trigger a callback when an item
893 NSString *key = @"values.selectedSaverName";
894 NSUserDefaultsController *prefs =
895 [NSUserDefaultsController sharedUserDefaultsController];
896 [prefs addObserver:self
899 context:@selector(selectedSaverDidChange:)];
900 [popup bind:@"selectedObject"
904 [prefs setAppliesImmediately:YES];
906 NSRect r = [popup frame];
907 r.size.width = max_width;
914 - (NSString *) makeDesc:(NSString *)saver
915 yearOnly:(BOOL) yearp
918 NSString *path = [saverDir stringByAppendingPathComponent:
919 [[saver lowercaseString]
920 stringByReplacingOccurrencesOfString:@" "
924 path = [path stringByAppendingPathExtension:@"xml"];
925 NSData *xmld = [NSData dataWithContentsOfFile:path];
926 if (! xmld) goto FAIL;
927 desc = [XScreenSaverView decompressXML:xmld];
928 if (! desc) goto FAIL;
930 r = [desc rangeOfString:@"<_description>"
931 options:NSCaseInsensitiveSearch];
936 desc = [desc substringFromIndex: r.location + r.length];
937 r = [desc rangeOfString:@"</_description>"
938 options:NSCaseInsensitiveSearch];
940 desc = [desc substringToIndex: r.location];
942 // Leading and trailing whitespace.
943 desc = [desc stringByTrimmingCharactersInSet:
944 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
946 // Let's see if we can find a year on the last line.
947 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
949 for (NSString *word in
950 [[desc substringFromIndex:r.location + r.length]
951 componentsSeparatedByCharactersInSet:
952 [NSCharacterSet characterSetWithCharactersInString:
954 int n = [word doubleValue];
955 if (n > 1970 && n < 2100)
959 // Delete everything after the first blank line.
961 r = [desc rangeOfString:@"\n\n" options:0];
963 desc = [desc substringToIndex: r.location];
965 // Unwrap lines and compress whitespace.
967 NSString *result = @"";
968 for (NSString *s in [desc componentsSeparatedByCharactersInSet:
969 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
970 if ([result length] == 0)
972 else if ([s length] > 0)
973 result = [NSString stringWithFormat: @"%@ %@", result, s];
979 desc = [year stringByAppendingString:
980 [@": " stringByAppendingString: desc]];
983 desc = year ? year : @"";
987 if ([saverNames count] > 1)
988 desc = @"Oops, this module appears to be incomplete.";
996 - (NSString *) makeDesc:(NSString *)saver
998 return [self makeDesc:saver yearOnly:NO];
1003 /* Create a dictionary of one-line descriptions of every saver,
1004 for display on the UITableView.
1006 - (NSDictionary *)makeDescTable
1008 NSMutableDictionary *dict =
1009 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1010 for (NSString *saver in saverNames) {
1011 [dict setObject:[self makeDesc:saver] forKey:saver];
1017 #endif // USE_IPHONE
1021 /* This is called when the "selectedSaverName" pref changes, e.g.,
1022 when a menu selection is made.
1024 - (void)observeValueForKeyPath:(NSString *)keyPath
1026 change:(NSDictionary *)change
1027 context:(void *)context
1029 SEL dispatchSelector = (SEL)context;
1030 if (dispatchSelector != NULL) {
1031 [self performSelector:dispatchSelector withObject:change];
1033 [super observeValueForKeyPath:keyPath
1043 /* Create the desktop window shell, possibly including a preferences button.
1045 - (NSWindow *) makeWindow
1048 static int count = 0;
1049 Bool simple_p = ([saverNames count] == 1);
1051 NSPopUpButton *menu = 0;
1056 sv_rect.origin.x = sv_rect.origin.y = 0;
1057 sv_rect.size.width = 320;
1058 sv_rect.size.height = 240;
1059 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
1060 initWithFrame:sv_rect
1063 // make a "Preferences" button
1068 rect.size.width = rect.size.height = 10;
1069 pb = [[NSButton alloc] initWithFrame:rect];
1070 [pb setTitle:@"Preferences"];
1071 [pb setBezelStyle:NSRoundedBezelStyle];
1074 rect.origin.x = ([sv frame].size.width -
1075 [pb frame].size.width) / 2;
1076 [pb setFrameOrigin:rect.origin];
1080 [pb setTarget:self];
1081 [pb setAction:@selector(openPreferences:)];
1083 // Make a saver selection menu
1085 menu = [self makeMenu];
1088 [menu setFrameOrigin:rect.origin];
1090 // make a box to wrap the saverView
1094 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1095 gbox = [[NSBox alloc] initWithFrame:rect];
1096 rect.size.width = rect.size.height = 10;
1097 [gbox setContentViewMargins:rect.size];
1098 [gbox setTitlePosition:NSNoTitle];
1099 [gbox addSubview:sv];
1102 // make a box to wrap the other two boxes
1104 rect.origin.x = rect.origin.y = 0;
1105 rect.size.width = [gbox frame].size.width;
1106 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1107 pbox = [[NSBox alloc] initWithFrame:rect];
1108 [pbox setTitlePosition:NSNoTitle];
1109 [pbox setBorderType:NSNoBorder];
1110 [pbox addSubview:gbox];
1111 if (menu) [pbox addSubview:menu];
1112 if (pb) [pbox addSubview:pb];
1115 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1116 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1117 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1118 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1121 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1124 // and make a window to hold that.
1126 NSScreen *screen = [NSScreen mainScreen];
1127 rect = pbox ? [pbox frame] : [sv frame];
1128 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1129 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1131 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1133 NSWindow *win = [[NSWindow alloc]
1134 initWithContentRect:rect
1135 styleMask:(NSTitledWindowMask |
1136 NSClosableWindowMask |
1137 NSMiniaturizableWindowMask |
1138 NSResizableWindowMask)
1139 backing:NSBackingStoreBuffered
1142 [win setMinSize:[win frameRectForContentRect:rect].size];
1143 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1145 [win makeKeyAndOrderFront:win];
1147 [sv startAnimation]; // this is the dummy saver
1157 for (NSWindow *win in windows) {
1158 ScreenSaverView *sv = find_saverView ([win contentView]);
1159 if ([sv isAnimating])
1160 [sv animateOneFrame];
1164 # endif // !USE_IPHONE
1167 - (void)applicationDidFinishLaunching:
1169 (NSNotification *) notif
1170 # else // USE_IPHONE
1171 (UIApplication *) application
1172 # endif // USE_IPHONE
1174 [self listSaverBundleNames];
1177 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1178 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1183 // Create either one window (for standalone, e.g. Phosphor.app)
1184 // or two windows for SaverTester.app.
1185 for (i = 0; i < window_count; i++) {
1186 NSWindow *win = [self makeWindow];
1187 // Get the last-saved window position out of preferences.
1188 [win setFrameAutosaveName:
1189 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1190 [win setFrameUsingName:[win frameAutosaveName]];
1192 // This prevents clicks from being seen by savers.
1193 // [win setMovableByWindowBackground:YES];
1195 # else // USE_IPHONE
1197 # undef ya_rand_init
1198 ya_rand_init (0); // Now's a good time.
1200 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1202 [window setRootViewController: rotating_nav];
1203 [window setAutoresizesSubviews:YES];
1204 [window setAutoresizingMask:
1205 (UIViewAutoresizingFlexibleWidth |
1206 UIViewAutoresizingFlexibleHeight)];
1208 nonrotating_nav = [[[RotateyViewController alloc] initWithRotation:NO]
1210 [nonrotating_nav setNavigationBarHidden:YES animated:NO];
1212 /* We run the saver on a different UIWindow than the one the
1213 SaverListController and preferences panels run on, because that's
1214 the only way to make rotation work right. We want the system to
1215 handle rotation of the UI stuff, but we want it to keep its hands
1216 off of rotation of the savers. As of iOS 8, this seems to be the
1217 only way to accomplish that.
1219 Also, we need to create saverWindow with a portrait rectangle, always.
1220 Note that [UIScreen bounds] returns rotated and scaled values.
1222 UIScreen *screen = [UIScreen mainScreen];
1223 # ifndef __IPHONE_8_0 // iOS 7 SDK
1224 NSRect frame = [screen bounds];
1225 int ss = [screen scale];
1227 NSRect frame = ([screen respondsToSelector:@selector(nativeBounds)]
1228 ? [screen nativeBounds] // iOS 8
1229 : [screen bounds]); // iOS 7
1230 int ss = ([screen respondsToSelector:@selector(nativeScale)]
1231 ? [screen nativeScale] // iOS 8
1232 : [screen scale]); // iOS 7
1233 # endif // iOS 8 SDK
1234 frame.size.width /= ss;
1235 frame.size.height /= ss;
1236 saverWindow = [[UIWindow alloc] initWithFrame:frame];
1237 [saverWindow setRootViewController: nonrotating_nav];
1238 [saverWindow setHidden:YES];
1240 /* This view is the parent of the XScreenSaverView, and exists only
1241 so that there is a black background behind it. Without this, when
1242 rotation is in progress, the scrolling-list window's corners show
1243 through in the corners.
1245 backgroundView = [[[NSView class] alloc] initWithFrame:[saverWindow frame]];
1246 [backgroundView setBackgroundColor:[NSColor blackColor]];
1248 SaverListController *menu = [[SaverListController alloc]
1249 initWithNames:saverNames
1250 descriptions:[self makeDescTable]];
1251 [rotating_nav pushViewController:menu animated:YES];
1252 [menu becomeFirstResponder];
1254 application.applicationSupportsShakeToEdit = YES;
1257 # endif // USE_IPHONE
1259 NSString *forced = 0;
1260 /* In the XCode project, each .saver scheme sets this env var when
1261 launching SaverTester.app so that it knows which one we are
1262 currently debugging. If this is set, it overrides the default
1263 selection in the popup menu. If unset, that menu persists to
1264 whatever it was last time.
1266 const char *f = getenv ("SELECTED_SAVER");
1268 forced = [NSString stringWithCString:(char *)f
1269 encoding:NSUTF8StringEncoding];
1271 if (forced && ![saverNames containsObject:forced]) {
1272 NSLog(@"forced saver \"%@\" does not exist", forced);
1276 // If there's only one saver, run that.
1277 if (!forced && [saverNames count] == 1)
1278 forced = [saverNames objectAtIndex:0];
1280 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1283 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1288 // If nothing was selected (e.g., this is the first launch)
1289 // then scroll randomly instead of starting up at "A".
1292 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1295 [menu scrollTo: prev];
1296 # endif // USE_IPHONE
1299 [prefs setObject:forced forKey:@"selectedSaverName"];
1302 /* Don't auto-launch the saver unless it was running last time.
1303 XScreenSaverView manages this, on crash_timer.
1306 if (!forced && ![prefs boolForKey:@"wasRunning"])
1310 [self selectedSaverDidChange:nil];
1311 // [NSTimer scheduledTimerWithTimeInterval: 0
1313 // selector:@selector(selectedSaverDidChange:)
1320 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1321 ScreenSaverView to run its own timer calling animateOneFrame.
1322 On 10.9, that fails because the private class ScreenSaverModule
1323 is only initialized properly by ScreenSaverEngine, and in the
1324 context of SaverRunner, the null ScreenSaverEngine instance
1325 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1326 So, if it looks like this is the 10.9 version of ScreenSaverModule
1327 instead of the 10.8 version, we run our own timer here. This sucks.
1330 Class ssm = NSClassFromString (@"ScreenSaverModule");
1331 if (ssm && [ssm instancesRespondToSelector:
1332 @selector(needsAnimationTimer)]) {
1333 NSWindow *win = [windows objectAtIndex:0];
1334 ScreenSaverView *sv = find_saverView ([win contentView]);
1335 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1336 [sv animationTimeInterval]
1338 selector:@selector(animTimer)
1343 # endif // !USE_IPHONE
1349 /* When the window closes, exit (even if prefs still open.)
1351 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1356 # else // USE_IPHONE
1358 - (void)applicationWillResignActive:(UIApplication *)app
1360 [(XScreenSaverView *)view setScreenLocked:YES];
1363 - (void)applicationDidBecomeActive:(UIApplication *)app
1365 [(XScreenSaverView *)view setScreenLocked:NO];
1368 - (void)applicationDidEnterBackground:(UIApplication *)application
1370 [(XScreenSaverView *)view setScreenLocked:YES];
1373 #endif // USE_IPHONE