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"
41 # define UIInterfaceOrientationUnknown UIDeviceOrientationUnknown
43 # ifndef NSFoundationVersionNumber_iOS_7_1
44 # define NSFoundationVersionNumber_iOS_7_1 1047.25
46 # ifndef NSFoundationVersionNumber_iOS_8_0
47 # define NSFoundationVersionNumber_iOS_8_0 1134.10
50 @interface RotateyViewController : UINavigationController
56 @implementation RotateyViewController
58 /* This subclass exists so that we can ask that the SaverListController and
59 preferences panels be auto-rotated by the system. Note that the
60 XScreenSaverView is not auto-rotated because it is on a different UIWindow.
63 - (id)initWithRotation:(BOOL)rotatep
66 allowRotation = rotatep;
70 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
72 return allowRotation; /* Deprecated in iOS 6 */
75 - (BOOL)shouldAutorotate /* Added in iOS 6 */
80 - (NSUInteger)supportedInterfaceOrientations /* Added in iOS 6 */
82 return UIInterfaceOrientationMaskAll;
88 @implementation SaverViewController
90 @synthesize saverName;
92 - (id)initWithSaverRunner:(SaverRunner *)parent
97 _storedOrientation = UIInterfaceOrientationUnknown;
99 self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
100 self.wantsFullScreenLayout = YES;
108 [_saverName release];
109 [_saverView dealloc];
116 // The UIViewController's view must never change, so it gets set here to
117 // a plain black background.
119 // This background view doesn't block the status bar, but that's probably
120 // OK, because it's never on screen for more than a fraction of a second.
121 UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull];
122 backgroundView.backgroundColor = [UIColor blackColor];
123 self.view = backgroundView;
127 - (void)createSaverView
129 UIView *parentView = self.view;
132 [_saverView removeFromSuperview];
133 [_saverView release];
136 if (_storedOrientation != UIInterfaceOrientationUnknown) {
137 [[UIApplication sharedApplication]
138 setStatusBarOrientation:_storedOrientation
142 _saverView = [_parent makeSaverView:_saverName
143 withSize:parentView.bounds.size];
146 [[[UIAlertView alloc] initWithTitle: _saverName
147 message: @"Unable to load!"
149 cancelButtonTitle: @"Bummer"
150 otherButtonTitles: nil]
155 _saverView.delegate = _parent;
157 [self.view addSubview:_saverView];
159 // The first responder must be set only after the view was placed in the view
161 [_saverView becomeFirstResponder]; // For shakes on iOS 6.
162 [_saverView startAnimation];
163 [_parent aboutPanel:_saverView orientation:_storedOrientation];
167 - (void)viewDidAppear:(BOOL)animated
169 [self createSaverView];
173 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
175 return NO; /* Deprecated in iOS 6 */
179 - (BOOL)shouldAutorotate /* Added in iOS 6 */
185 - (NSUInteger)supportedInterfaceOrientations /* Added in iOS 6 */
187 // Lies from the iOS docs:
188 // "This method is only called if the view controller's shouldAutorotate
189 // method returns YES."
190 return UIInterfaceOrientationMaskPortrait;
194 - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
196 return UIInterfaceOrientationPortrait;
200 - (void)setSaverName:(NSString *)name
203 [_saverName release];
205 _storedOrientation = [UIApplication sharedApplication].statusBarOrientation;
208 [self createSaverView];
216 @implementation SaverRunner
219 - (XScreenSaverView *) makeSaverView: (NSString *) module
220 withSize: (NSSize) size
226 // Load the XScreenSaverView subclass and code from a ".saver" bundle.
228 NSString *name = [module stringByAppendingPathExtension:@"saver"];
229 NSString *path = [saverDir stringByAppendingPathComponent:name];
231 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
232 NSLog(@"bundle \"%@\" does not exist", path);
236 NSLog(@"Loading %@", path);
238 // NSBundle *obundle = saverBundle;
240 saverBundle = [NSBundle bundleWithPath:path];
242 new_class = [saverBundle principalClass];
244 // Not entirely unsurprisingly, this tends to break the world.
245 // if (obundle && obundle != saverBundle)
250 // Determine whether to create an X11 view or an OpenGL view by
251 // looking for the "gl" tag in the xml file. This is kind of awful.
253 NSString *path = [saverDir
254 stringByAppendingPathComponent:
255 [[[module lowercaseString]
256 stringByReplacingOccurrencesOfString:@" "
258 stringByAppendingPathExtension:@"xml"]];
259 NSData *xmld = [NSData dataWithContentsOfFile:path];
260 NSAssert (xmld, @"no XML: %@", path);
261 NSString *xml = [XScreenSaverView decompressXML:xmld];
262 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
265 ? [XScreenSaverGLView class]
266 : [XScreenSaverView class]);
268 # endif // USE_IPHONE
274 rect.origin.x = rect.origin.y = 0;
275 rect.size.width = size.width;
276 rect.size.height = size.height;
278 XScreenSaverView *instance =
279 [(XScreenSaverView *) [new_class alloc]
284 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
289 /* KLUGE: Inform the underlying program that we're in "standalone"
290 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
291 This is kind of horrible but I haven't thought of a more sensible
292 way to make this work.
295 if ([saverNames count] == 1) {
296 putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
300 return (XScreenSaverView *) instance;
306 static ScreenSaverView *
307 find_saverView_child (NSView *v)
309 NSArray *kids = [v subviews];
310 int nkids = [kids count];
312 for (i = 0; i < nkids; i++) {
313 NSObject *kid = [kids objectAtIndex:i];
314 if ([kid isKindOfClass:[ScreenSaverView class]]) {
315 return (ScreenSaverView *) kid;
317 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
325 static ScreenSaverView *
326 find_saverView (NSView *v)
329 NSView *p = [v superview];
333 return find_saverView_child (v);
337 /* Changes the contents of the menubar menus to correspond to
338 the running saver. Desktop only.
341 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
343 if ([v isKindOfClass:[NSMenu class]]) {
344 NSMenu *m = (NSMenu *)v;
345 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
346 withString:new_str]];
347 NSArray *kids = [m itemArray];
348 int nkids = [kids count];
350 for (i = 0; i < nkids; i++) {
351 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
353 } else if ([v isKindOfClass:[NSMenuItem class]]) {
354 NSMenuItem *mi = (NSMenuItem *)v;
355 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
356 withString:new_str]];
357 NSMenu *m = [mi submenu];
358 if (m) relabel_menus (m, old_str, new_str);
363 - (void) openPreferences: (id) sender
366 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
367 sv = find_saverView ((NSView *) sender);
371 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
372 w = [windows objectAtIndex:i];
373 if ([w isKeyWindow]) break;
375 sv = find_saverView ([w contentView]);
378 NSAssert (sv, @"no saver view");
380 NSWindow *prefs = [sv configureSheet];
382 [NSApp beginSheet:prefs
383 modalForWindow:[sv window]
385 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
387 int code = [NSApp runModalForWindow:prefs];
389 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
390 We have to restart *both* animations, because the xlockmore-style
391 ones will blow up if one re-inits but the other doesn't.
393 if (code != NSCancelButton) {
394 if ([sv isAnimating])
401 - (void) preferencesClosed: (NSWindow *) sheet
402 returnCode: (int) returnCode
403 contextInfo: (void *) contextInfo
405 [NSApp stopModalWithCode:returnCode];
411 - (UIImage *) screenshot
413 return saved_screenshot;
416 - (void) saveScreenshot
418 // Most of this is from:
419 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
420 // The rotation stuff is by me.
422 CGSize size = [[UIScreen mainScreen] bounds].size;
424 // iOS 7: Needs to be the actual device orientation.
425 // iOS 8: Needs to be UIInterfaceOrientationPortrait.
427 UIInterfaceOrientation orient =
428 NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 ?
429 UIInterfaceOrientationPortrait /* iOS 8 broke -[UIScreen bounds]. */ :
430 [[window rootViewController] interfaceOrientation];
432 if (orient == UIInterfaceOrientationLandscapeLeft ||
433 orient == UIInterfaceOrientationLandscapeRight) {
434 // Rotate the shape of the canvas 90 degrees.
435 double s = size.width;
436 size.width = size.height;
441 // Create a graphics context with the target size
442 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
443 // take the scale into consideration
444 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
446 if (UIGraphicsBeginImageContextWithOptions)
447 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
449 UIGraphicsBeginImageContext (size);
451 CGContextRef ctx = UIGraphicsGetCurrentContext();
454 // Rotate the graphics context to match current hardware rotation.
457 case UIInterfaceOrientationPortraitUpsideDown:
458 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
459 CGContextRotateCTM (ctx, M_PI);
460 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
462 case UIInterfaceOrientationLandscapeLeft:
463 case UIInterfaceOrientationLandscapeRight:
464 CGContextTranslateCTM (ctx,
465 ([window frame].size.height -
466 [window frame].size.width) / 2,
467 ([window frame].size.width -
468 [window frame].size.height) / 2);
469 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
470 CGContextRotateCTM (ctx,
471 (orient == UIInterfaceOrientationLandscapeLeft
474 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
480 // Iterate over every window from back to front
482 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
483 if (![win respondsToSelector:@selector(screen)] ||
484 [win screen] == [UIScreen mainScreen]) {
486 // -renderInContext: renders in the coordinate space of the layer,
487 // so we must first apply the layer's geometry to the graphics context
488 CGContextSaveGState (ctx);
490 // Center the context around the window's anchor point
491 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
493 // Apply the window's transform about the anchor point
494 CGContextConcatCTM (ctx, [win transform]);
496 // Offset by the portion of the bounds left of and above anchor point
497 CGContextTranslateCTM (ctx,
498 -[win bounds].size.width * [[win layer] anchorPoint].x,
499 -[win bounds].size.height * [[win layer] anchorPoint].y);
501 // Render the layer hierarchy to the current context
502 [[win layer] renderInContext:ctx];
504 // Restore the context
505 CGContextRestoreGState (ctx);
509 if (saved_screenshot)
510 [saved_screenshot release];
511 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
513 UIGraphicsEndImageContext();
517 - (void) openPreferences: (NSString *) saver
519 XScreenSaverView *saverView = [self makeSaverView:saver
520 withSize:CGSizeMake(0, 0)];
521 if (! saverView) return;
523 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
524 [prefs setObject:saver forKey:@"selectedSaverName"];
527 [rotating_nav pushViewController: [saverView configureView]
538 - (void)loadSaver:(NSString *)name
542 if (saverName && [saverName isEqualToString: name]) {
543 for (NSWindow *win in windows) {
544 ScreenSaverView *sv = find_saverView ([win contentView]);
545 if (![sv isAnimating])
553 for (NSWindow *win in windows) {
554 NSView *cv = [win contentView];
555 NSString *old_title = [win title];
556 if (!old_title) old_title = @"XScreenSaver";
557 [win setTitle: name];
558 relabel_menus (menubar, old_title, name);
560 ScreenSaverView *old_view = find_saverView (cv);
561 NSView *sup = old_view ? [old_view superview] : cv;
564 if ([old_view isAnimating])
565 [old_view stopAnimation];
566 [old_view removeFromSuperview];
569 NSSize size = [cv frame].size;
570 ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
571 NSAssert (new_view, @"unable to make a saver view");
573 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
574 [sup addSubview: new_view];
575 [win makeFirstResponder:new_view];
576 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
578 [new_view startAnimation];
581 NSUserDefaultsController *ctl =
582 [NSUserDefaultsController sharedUserDefaultsController];
587 # if TARGET_IPHONE_SIMULATOR
588 NSLog (@"selecting saver \"%@\"", name);
591 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
592 [prefs setObject:name forKey:@"selectedSaverName"];
595 /* Cacheing this screws up rotation when starting a saver twice in a row.
596 if (saverName && [saverName isEqualToString: name]) {
597 if ([saverView isAnimating])
606 if (nonrotating_controller) {
607 nonrotating_controller.saverName = name;
611 # if TARGET_IPHONE_SIMULATOR
612 UIScreen *screen = [UIScreen mainScreen];
614 /* 'nativeScale' is very confusing.
617 bounds: 320x480 scale: 2
618 nativeBounds: 640x960 nativeScale: 2
620 bounds: 320x568 scale: 2
621 nativeBounds: 640x1136 nativeScale: 2
623 bounds: 768x1024 scale: 1
624 nativeBounds: 768x1024 nativeScale: 1
626 bounds: 768x1024 scale: 2
627 nativeBounds: 1536x2048 nativeScale: 2
629 bounds: 320x568 scale: 2
630 nativeBounds: 640x1136 nativeScale: 2
632 bounds: 320x568 scale: 2
633 nativeBounds: 960x1704 nativeScale: 3
635 According to a StackOverflow comment:
637 The iPhone 6+ renders internally using @3x assets at a virtual
638 resolution of 2208x1242 (with 736x414 points), then samples that down
639 for display. The same as using a scaled resolution on a Retina MacBook
640 -- it lets them hit an integral multiple for pixel assets while still
641 having e.g. 12pt text look the same size on the screen.
643 The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
644 and use @2x assets to stick to the approximately 160 points per inch
645 of all previous devices.
647 The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
648 @2.46x assets. Instead Apple uses @3x assets and scales the complete
649 output down to about 84% of its natural size.
651 In practice Apple has decided to go with more like 87%, turning the
652 1080 into 1242. No doubt that was to find something as close as
653 possible to 84% that still produced integral sizes in both directions
654 -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
655 into, say, 1286, you'd somehow need to render 2286.22 pixels
656 vertically to scale well.
659 NSLog(@"screen: %.0fx%0.f",
660 [[screen currentMode] size].width,
661 [[screen currentMode] size].height);
662 NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
663 [screen bounds].size.width,
664 [screen bounds].size.height,
666 [screen scale] * [screen bounds].size.width,
667 [screen scale] * [screen bounds].size.height);
670 if ([screen respondsToSelector:@selector(nativeBounds)])
671 NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
672 [screen nativeBounds].size.width,
673 [screen nativeBounds].size.height,
674 [screen nativeScale],
675 [screen nativeBounds].size.width / [screen nativeScale],
676 [screen nativeBounds].size.height / [screen nativeScale]);
678 # endif // TARGET_IPHONE_SIMULATOR
680 // Take the screen shot before creating the screen saver view, because this
681 // can screw with the layout.
682 [self saveScreenshot];
684 // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal
685 // presentation full screen.
686 rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen;
688 nonrotating_controller = [[SaverViewController alloc] initWithSaverRunner:self];
689 nonrotating_controller.saverName = name;
693 [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil];
695 // Doing this makes savers cut back to the list instead of fading,
696 // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
697 // [window setHidden:YES];
699 # endif // USE_IPHONE
705 - (void)aboutPanel:(id)sender
707 NSDictionary *bd = [saverBundle infoDictionary];
708 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
710 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
711 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
712 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
713 forKey:@"ApplicationVersion"];
714 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
715 [d setValue:[[NSAttributedString alloc]
716 initWithString: (NSString *)
717 [bd objectForKey:@"CFBundleGetInfoString"]]
720 [[NSApplication sharedApplication]
721 orderFrontStandardAboutPanelWithOptions:d];
726 - (void)aboutPanel:(UIView *)saverView
727 orientation:(UIInterfaceOrientation)orient
729 if ([saverNames count] == 1)
732 NSString *name = saverName;
733 NSString *year = [self makeDesc:saverName yearOnly:YES];
736 CGRect frame = [saverView frame];
740 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
741 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
744 CGSize s = CGSizeMake(frame.size.width, frame.size.height);
745 CGSize tsize1 = [[[NSAttributedString alloc]
747 attributes:@{ NSFontAttributeName: font1 }]
748 boundingRectWithSize: s
749 options: NSStringDrawingUsesLineFragmentOrigin
751 CGSize tsize2 = [[[NSAttributedString alloc]
753 attributes:@{ NSFontAttributeName: font2 }]
754 boundingRectWithSize: s
755 options: NSStringDrawingUsesLineFragmentOrigin
757 # else // iOS 6 or Cocoa
758 CGSize tsize1 = [name sizeWithFont:font1
759 constrainedToSize:CGSizeMake(frame.size.width,
761 CGSize tsize2 = [year sizeWithFont:font2
762 constrainedToSize:CGSizeMake(frame.size.width,
766 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
767 tsize1.width : tsize2.width,
768 tsize1.height + tsize2.height);
770 tsize.width = ceilf(tsize.width);
771 tsize.height = ceilf(tsize.height);
773 // Don't know how to find inner margin of UITextView.
775 tsize.width += margin * 4;
776 tsize.height += margin * 2;
778 if ([saverView frame].size.width >= 768)
779 tsize.height += pt1 * 3; // extra bottom margin on iPad
781 frame = CGRectMake (0, 0, tsize.width, tsize.height);
783 /* Get the text oriented properly, and move it to the bottom of the
784 screen, since many savers have action in the middle.
787 case UIInterfaceOrientationLandscapeLeft:
789 frame.origin.x = ([saverView frame].size.width
790 - (tsize.width - tsize.height) / 2
792 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
794 case UIInterfaceOrientationLandscapeRight:
796 frame.origin.x = -(tsize.width - tsize.height) / 2;
797 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
799 case UIInterfaceOrientationPortraitUpsideDown:
801 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
806 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
807 frame.origin.y = [saverView frame].size.height - tsize.height;
812 [aboutBox removeFromSuperview];
814 aboutBox = [[UIView alloc] initWithFrame:frame];
816 aboutBox.transform = CGAffineTransformMakeRotation (rot);
817 aboutBox.backgroundColor = [UIColor clearColor];
819 /* There seems to be no easy way to stroke the font, so instead draw
820 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
821 a black shadow to each. (You'd think the shadow alone would be
822 enough, but there's no way to make it dark enough to be legible.)
824 for (int i = 0; i < 5; i++) {
825 UITextView *textview;
827 frame.origin.x = frame.origin.y = 0;
829 case 0: frame.origin.x = -off; break;
830 case 1: frame.origin.x = off; break;
831 case 2: frame.origin.y = -off; break;
832 case 3: frame.origin.y = off; break;
835 for (int j = 0; j < 2; j++) {
837 frame.origin.y = (j == 0 ? 0 : pt1);
838 textview = [[UITextView alloc] initWithFrame:frame];
839 textview.font = (j == 0 ? font1 : font2);
840 textview.text = (j == 0 ? name : year);
841 textview.textAlignment = NSTextAlignmentCenter;
842 textview.showsHorizontalScrollIndicator = NO;
843 textview.showsVerticalScrollIndicator = NO;
844 textview.scrollEnabled = NO;
845 textview.editable = NO;
846 textview.userInteractionEnabled = NO;
847 textview.backgroundColor = [UIColor clearColor];
848 textview.textColor = (i == 4
849 ? [UIColor yellowColor]
850 : [UIColor blackColor]);
852 CALayer *textLayer = (CALayer *)
853 [textview.layer.sublayers objectAtIndex:0];
854 textLayer.shadowColor = [UIColor blackColor].CGColor;
855 textLayer.shadowOffset = CGSizeMake(0, 0);
856 textLayer.shadowOpacity = 1;
857 textLayer.shadowRadius = 2;
859 [aboutBox addSubview:textview];
863 CABasicAnimation *anim =
864 [CABasicAnimation animationWithKeyPath:@"opacity"];
866 anim.repeatCount = 1;
867 anim.autoreverses = NO;
868 anim.fromValue = [NSNumber numberWithFloat:0.0];
869 anim.toValue = [NSNumber numberWithFloat:1.0];
870 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
872 [saverView addSubview:aboutBox];
875 [splashTimer invalidate];
878 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
880 selector:@selector(aboutOff)
890 [splashTimer invalidate];
893 CABasicAnimation *anim =
894 [CABasicAnimation animationWithKeyPath:@"opacity"];
896 anim.repeatCount = 1;
897 anim.autoreverses = NO;
898 anim.fromValue = [NSNumber numberWithFloat: 1];
899 anim.toValue = [NSNumber numberWithFloat: 0];
900 anim.delegate = self;
901 aboutBox.layer.opacity = 0;
902 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
909 - (void)selectedSaverDidChange:(NSDictionary *)change
911 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
912 NSString *name = [prefs stringForKey:@"selectedSaverName"];
916 if (! [saverNames containsObject:name]) {
917 NSLog (@"saver \"%@\" does not exist", name);
921 [self loadSaver: name];
925 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
928 NSString *ext = @"saver";
930 NSString *ext = @"xml";
933 NSArray *files = [[NSFileManager defaultManager]
934 contentsOfDirectoryAtPath:dir error:nil];
935 if (! files) return 0;
936 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
938 for (NSString *p in files) {
939 if ([[p pathExtension] caseInsensitiveCompare: ext])
942 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
945 // Get the saver name's capitalization right by reading the XML file.
947 p = [dir stringByAppendingPathComponent: p];
948 NSData *xmld = [NSData dataWithContentsOfFile:p];
949 NSAssert (xmld, @"no XML: %@", p);
950 NSString *xml = [XScreenSaverView decompressXML:xmld];
951 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
952 NSAssert1 (r.length, @"no name in %@", p);
954 xml = [xml substringFromIndex: r.location + r.length];
955 r = [xml rangeOfString:@"\"" options:0];
956 if (r.length) name = [xml substringToIndex: r.location];
959 # endif // USE_IPHONE
961 NSAssert1 (name, @"no name in %@", p);
962 if (name) [result addObject: name];
965 if (! [result count])
973 - (NSArray *) listSaverBundleNames
975 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
978 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
979 // directories in the bundle.
980 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
981 stringByAppendingPathComponent:@"Contents"]
982 stringByAppendingPathComponent:@"Resources"]];
983 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
985 // Also look in the same directory as the executable.
986 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
987 stringByDeletingLastPathComponent]];
989 // Finally, look in standard MacOS screensaver directories.
990 // [dirs addObject: @"~/Library/Screen Savers"];
991 // [dirs addObject: @"/Library/Screen Savers"];
992 // [dirs addObject: @"/System/Library/Screen Savers"];
996 // On iOS, only look in the bundle's root directory.
997 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
999 # endif // USE_IPHONE
1002 for (i = 0; i < [dirs count]; i++) {
1003 NSString *dir = [dirs objectAtIndex:i];
1004 NSArray *names = [self listSaverBundleNamesInDir:dir];
1005 if (! names) continue;
1006 saverDir = [dir retain];
1007 saverNames = [names retain];
1011 NSString *err = @"no .saver bundles found in: ";
1012 for (i = 0; i < [dirs count]; i++) {
1013 if (i) err = [err stringByAppendingString:@", "];
1014 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
1015 stringByAbbreviatingWithTildeInPath]];
1016 err = [err stringByAppendingString:@"/"];
1019 return [NSArray array];
1023 /* Create the popup menu of available saver names.
1027 - (NSPopUpButton *) makeMenu
1030 rect.origin.x = rect.origin.y = 0;
1031 rect.size.width = 10;
1032 rect.size.height = 10;
1033 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1036 float max_width = 0;
1037 for (i = 0; i < [saverNames count]; i++) {
1038 NSString *name = [saverNames objectAtIndex:i];
1039 [popup addItemWithTitle:name];
1040 [[popup itemWithTitle:name] setRepresentedObject:name];
1042 NSRect r = [popup frame];
1043 if (r.size.width > max_width) max_width = r.size.width;
1046 // Bind the menu to preferences, and trigger a callback when an item
1049 NSString *key = @"values.selectedSaverName";
1050 NSUserDefaultsController *prefs =
1051 [NSUserDefaultsController sharedUserDefaultsController];
1052 [prefs addObserver:self
1055 context:@selector(selectedSaverDidChange:)];
1056 [popup bind:@"selectedObject"
1060 [prefs setAppliesImmediately:YES];
1062 NSRect r = [popup frame];
1063 r.size.width = max_width;
1070 - (NSString *) makeDesc:(NSString *)saver
1071 yearOnly:(BOOL) yearp
1074 NSString *path = [saverDir stringByAppendingPathComponent:
1075 [[saver lowercaseString]
1076 stringByReplacingOccurrencesOfString:@" "
1080 path = [path stringByAppendingPathExtension:@"xml"];
1081 NSData *xmld = [NSData dataWithContentsOfFile:path];
1082 if (! xmld) goto FAIL;
1083 desc = [XScreenSaverView decompressXML:xmld];
1084 if (! desc) goto FAIL;
1086 r = [desc rangeOfString:@"<_description>"
1087 options:NSCaseInsensitiveSearch];
1088 if (r.length == 0) {
1092 desc = [desc substringFromIndex: r.location + r.length];
1093 r = [desc rangeOfString:@"</_description>"
1094 options:NSCaseInsensitiveSearch];
1096 desc = [desc substringToIndex: r.location];
1098 // Leading and trailing whitespace.
1099 desc = [desc stringByTrimmingCharactersInSet:
1100 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1102 // Let's see if we can find a year on the last line.
1103 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
1105 for (NSString *word in
1106 [[desc substringFromIndex:r.location + r.length]
1107 componentsSeparatedByCharactersInSet:
1108 [NSCharacterSet characterSetWithCharactersInString:
1110 int n = [word doubleValue];
1111 if (n > 1970 && n < 2100)
1115 // Delete everything after the first blank line.
1117 r = [desc rangeOfString:@"\n\n" options:0];
1119 desc = [desc substringToIndex: r.location];
1121 // Unwrap lines and compress whitespace.
1123 NSString *result = @"";
1124 for (NSString *s in [desc componentsSeparatedByCharactersInSet:
1125 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
1126 if ([result length] == 0)
1128 else if ([s length] > 0)
1129 result = [NSString stringWithFormat: @"%@ %@", result, s];
1135 desc = [year stringByAppendingString:
1136 [@": " stringByAppendingString: desc]];
1139 desc = year ? year : @"";
1143 if ([saverNames count] > 1)
1144 desc = @"Oops, this module appears to be incomplete.";
1152 - (NSString *) makeDesc:(NSString *)saver
1154 return [self makeDesc:saver yearOnly:NO];
1159 /* Create a dictionary of one-line descriptions of every saver,
1160 for display on the UITableView.
1162 - (NSDictionary *)makeDescTable
1164 NSMutableDictionary *dict =
1165 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1166 for (NSString *saver in saverNames) {
1167 [dict setObject:[self makeDesc:saver] forKey:saver];
1173 - (void) wantsFadeOut:(XScreenSaverView *)sender
1175 rotating_nav.view.hidden = NO; // In case it was hidden during startup.
1177 /* The XScreenSaverView screws with the status bar orientation, mostly to
1178 keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0
1179 and/or 8.2), this confuses the UINavigationController, so put the
1180 orientation back to portrait before dismissing the SaverViewController.
1182 [[UIApplication sharedApplication]
1183 setStatusBarOrientation:UIInterfaceOrientationPortrait
1186 [rotating_nav dismissViewControllerAnimated:YES completion:^() {
1187 [nonrotating_controller release];
1188 nonrotating_controller = nil;
1189 [[rotating_nav view] becomeFirstResponder];
1194 - (void) didShake:(XScreenSaverView *)sender
1196 # if TARGET_IPHONE_SIMULATOR
1197 NSLog (@"simulating shake on saver list");
1199 [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
1204 #endif // USE_IPHONE
1208 /* This is called when the "selectedSaverName" pref changes, e.g.,
1209 when a menu selection is made.
1211 - (void)observeValueForKeyPath:(NSString *)keyPath
1213 change:(NSDictionary *)change
1214 context:(void *)context
1216 SEL dispatchSelector = (SEL)context;
1217 if (dispatchSelector != NULL) {
1218 [self performSelector:dispatchSelector withObject:change];
1220 [super observeValueForKeyPath:keyPath
1230 /* Create the desktop window shell, possibly including a preferences button.
1232 - (NSWindow *) makeWindow
1235 static int count = 0;
1236 Bool simple_p = ([saverNames count] == 1);
1238 NSPopUpButton *menu = 0;
1243 sv_rect.origin.x = sv_rect.origin.y = 0;
1244 sv_rect.size.width = 320;
1245 sv_rect.size.height = 240;
1246 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
1247 initWithFrame:sv_rect
1250 // make a "Preferences" button
1255 rect.size.width = rect.size.height = 10;
1256 pb = [[NSButton alloc] initWithFrame:rect];
1257 [pb setTitle:@"Preferences"];
1258 [pb setBezelStyle:NSRoundedBezelStyle];
1261 rect.origin.x = ([sv frame].size.width -
1262 [pb frame].size.width) / 2;
1263 [pb setFrameOrigin:rect.origin];
1267 [pb setTarget:self];
1268 [pb setAction:@selector(openPreferences:)];
1270 // Make a saver selection menu
1272 menu = [self makeMenu];
1275 [menu setFrameOrigin:rect.origin];
1277 // make a box to wrap the saverView
1281 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1282 gbox = [[NSBox alloc] initWithFrame:rect];
1283 rect.size.width = rect.size.height = 10;
1284 [gbox setContentViewMargins:rect.size];
1285 [gbox setTitlePosition:NSNoTitle];
1286 [gbox addSubview:sv];
1289 // make a box to wrap the other two boxes
1291 rect.origin.x = rect.origin.y = 0;
1292 rect.size.width = [gbox frame].size.width;
1293 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1294 pbox = [[NSBox alloc] initWithFrame:rect];
1295 [pbox setTitlePosition:NSNoTitle];
1296 [pbox setBorderType:NSNoBorder];
1297 [pbox addSubview:gbox];
1298 if (menu) [pbox addSubview:menu];
1299 if (pb) [pbox addSubview:pb];
1302 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1303 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1304 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1305 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1308 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1311 // and make a window to hold that.
1313 NSScreen *screen = [NSScreen mainScreen];
1314 rect = pbox ? [pbox frame] : [sv frame];
1315 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1316 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1318 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1320 NSWindow *win = [[NSWindow alloc]
1321 initWithContentRect:rect
1322 styleMask:(NSTitledWindowMask |
1323 NSClosableWindowMask |
1324 NSMiniaturizableWindowMask |
1325 NSResizableWindowMask)
1326 backing:NSBackingStoreBuffered
1329 [win setMinSize:[win frameRectForContentRect:rect].size];
1330 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1332 [win makeKeyAndOrderFront:win];
1334 [sv startAnimation]; // this is the dummy saver
1344 for (NSWindow *win in windows) {
1345 ScreenSaverView *sv = find_saverView ([win contentView]);
1346 if ([sv isAnimating])
1347 [sv animateOneFrame];
1351 # endif // !USE_IPHONE
1354 - (void)applicationDidFinishLaunching:
1356 (NSNotification *) notif
1357 # else // USE_IPHONE
1358 (UIApplication *) application
1359 # endif // USE_IPHONE
1361 [self listSaverBundleNames];
1363 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1366 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1367 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1372 // Create either one window (for standalone, e.g. Phosphor.app)
1373 // or two windows for SaverTester.app.
1374 for (i = 0; i < window_count; i++) {
1375 NSWindow *win = [self makeWindow];
1376 // Get the last-saved window position out of preferences.
1377 [win setFrameAutosaveName:
1378 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1379 [win setFrameUsingName:[win frameAutosaveName]];
1381 // This prevents clicks from being seen by savers.
1382 // [win setMovableByWindowBackground:YES];
1384 # else // USE_IPHONE
1386 # undef ya_rand_init
1387 ya_rand_init (0); // Now's a good time.
1391 "You must call this method before attempting to get orientation data from
1392 the receiver. This method enables the device's accelerometer hardware
1393 and begins the delivery of acceleration events to the receiver."
1395 Adding or removing this doesn't seem to make any difference. It's
1396 probably getting called by the UINavigationController. Still... */
1397 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
1399 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1402 if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
1403 rotating_nav.view.hidden = YES;
1405 [window setRootViewController: rotating_nav];
1406 [window setAutoresizesSubviews:YES];
1407 [window setAutoresizingMask:
1408 (UIViewAutoresizingFlexibleWidth |
1409 UIViewAutoresizingFlexibleHeight)];
1411 SaverListController *menu = [[SaverListController alloc]
1412 initWithNames:saverNames
1413 descriptions:[self makeDescTable]];
1414 [rotating_nav pushViewController:menu animated:YES];
1415 [menu becomeFirstResponder];
1417 application.applicationSupportsShakeToEdit = YES;
1420 # endif // USE_IPHONE
1422 NSString *forced = 0;
1423 /* In the XCode project, each .saver scheme sets this env var when
1424 launching SaverTester.app so that it knows which one we are
1425 currently debugging. If this is set, it overrides the default
1426 selection in the popup menu. If unset, that menu persists to
1427 whatever it was last time.
1429 const char *f = getenv ("SELECTED_SAVER");
1431 forced = [NSString stringWithCString:(char *)f
1432 encoding:NSUTF8StringEncoding];
1434 if (forced && ![saverNames containsObject:forced]) {
1435 NSLog(@"forced saver \"%@\" does not exist", forced);
1439 // If there's only one saver, run that.
1440 if (!forced && [saverNames count] == 1)
1441 forced = [saverNames objectAtIndex:0];
1444 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1449 // If nothing was selected (e.g., this is the first launch)
1450 // then scroll randomly instead of starting up at "A".
1453 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1456 [menu scrollTo: prev];
1457 # endif // USE_IPHONE
1460 [prefs setObject:forced forKey:@"selectedSaverName"];
1463 /* Don't auto-launch the saver unless it was running last time.
1464 XScreenSaverView manages this, on crash_timer.
1467 if (!forced && ![prefs boolForKey:@"wasRunning"])
1471 [self selectedSaverDidChange:nil];
1472 // [NSTimer scheduledTimerWithTimeInterval: 0
1474 // selector:@selector(selectedSaverDidChange:)
1481 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1482 ScreenSaverView to run its own timer calling animateOneFrame.
1483 On 10.9, that fails because the private class ScreenSaverModule
1484 is only initialized properly by ScreenSaverEngine, and in the
1485 context of SaverRunner, the null ScreenSaverEngine instance
1486 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1487 So, if it looks like this is the 10.9 version of ScreenSaverModule
1488 instead of the 10.8 version, we run our own timer here. This sucks.
1491 Class ssm = NSClassFromString (@"ScreenSaverModule");
1492 if (ssm && [ssm instancesRespondToSelector:
1493 @selector(needsAnimationTimer)]) {
1494 NSWindow *win = [windows objectAtIndex:0];
1495 ScreenSaverView *sv = find_saverView ([win contentView]);
1496 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1497 [sv animationTimeInterval]
1499 selector:@selector(animTimer)
1504 # endif // !USE_IPHONE
1510 /* When the window closes, exit (even if prefs still open.)
1512 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1517 # else // USE_IPHONE
1519 - (void)applicationWillResignActive:(UIApplication *)app
1521 [(XScreenSaverView *)view setScreenLocked:YES];
1524 - (void)applicationDidBecomeActive:(UIApplication *)app
1526 [(XScreenSaverView *)view setScreenLocked:NO];
1529 - (void)applicationDidEnterBackground:(UIApplication *)application
1531 [(XScreenSaverView *)view setScreenLocked:YES];
1534 #endif // USE_IPHONE