1 /* xscreensaver, Copyright (c) 2006-2015 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];
947 // Do not show TestX11 in release builds.
948 if (! [name caseInsensitiveCompare:@"testx11"])
951 // Get the saver name's capitalization right by reading the XML file.
953 p = [dir stringByAppendingPathComponent: p];
954 NSData *xmld = [NSData dataWithContentsOfFile:p];
955 NSAssert (xmld, @"no XML: %@", p);
956 NSString *xml = [XScreenSaverView decompressXML:xmld];
957 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
958 NSAssert1 (r.length, @"no name in %@", p);
960 xml = [xml substringFromIndex: r.location + r.length];
961 r = [xml rangeOfString:@"\"" options:0];
962 if (r.length) name = [xml substringToIndex: r.location];
965 # endif // USE_IPHONE
967 NSAssert1 (name, @"no name in %@", p);
968 if (name) [result addObject: name];
971 if (! [result count])
979 - (NSArray *) listSaverBundleNames
981 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
984 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
985 // directories in the bundle.
986 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
987 stringByAppendingPathComponent:@"Contents"]
988 stringByAppendingPathComponent:@"Resources"]];
989 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
991 // Also look in the same directory as the executable.
992 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
993 stringByDeletingLastPathComponent]];
995 // Finally, look in standard MacOS screensaver directories.
996 // [dirs addObject: @"~/Library/Screen Savers"];
997 // [dirs addObject: @"/Library/Screen Savers"];
998 // [dirs addObject: @"/System/Library/Screen Savers"];
1000 # else // USE_IPHONE
1002 // On iOS, only look in the bundle's root directory.
1003 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
1005 # endif // USE_IPHONE
1008 for (i = 0; i < [dirs count]; i++) {
1009 NSString *dir = [dirs objectAtIndex:i];
1010 NSArray *names = [self listSaverBundleNamesInDir:dir];
1011 if (! names) continue;
1012 saverDir = [dir retain];
1013 saverNames = [names retain];
1017 NSString *err = @"no .saver bundles found in: ";
1018 for (i = 0; i < [dirs count]; i++) {
1019 if (i) err = [err stringByAppendingString:@", "];
1020 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
1021 stringByAbbreviatingWithTildeInPath]];
1022 err = [err stringByAppendingString:@"/"];
1025 return [NSArray array];
1029 /* Create the popup menu of available saver names.
1033 - (NSPopUpButton *) makeMenu
1036 rect.origin.x = rect.origin.y = 0;
1037 rect.size.width = 10;
1038 rect.size.height = 10;
1039 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1042 float max_width = 0;
1043 for (i = 0; i < [saverNames count]; i++) {
1044 NSString *name = [saverNames objectAtIndex:i];
1045 [popup addItemWithTitle:name];
1046 [[popup itemWithTitle:name] setRepresentedObject:name];
1048 NSRect r = [popup frame];
1049 if (r.size.width > max_width) max_width = r.size.width;
1052 // Bind the menu to preferences, and trigger a callback when an item
1055 NSString *key = @"values.selectedSaverName";
1056 NSUserDefaultsController *prefs =
1057 [NSUserDefaultsController sharedUserDefaultsController];
1058 [prefs addObserver:self
1061 context:@selector(selectedSaverDidChange:)];
1062 [popup bind:@"selectedObject"
1066 [prefs setAppliesImmediately:YES];
1068 NSRect r = [popup frame];
1069 r.size.width = max_width;
1076 - (NSString *) makeDesc:(NSString *)saver
1077 yearOnly:(BOOL) yearp
1080 NSString *path = [saverDir stringByAppendingPathComponent:
1081 [[saver lowercaseString]
1082 stringByReplacingOccurrencesOfString:@" "
1086 path = [path stringByAppendingPathExtension:@"xml"];
1087 NSData *xmld = [NSData dataWithContentsOfFile:path];
1088 if (! xmld) goto FAIL;
1089 desc = [XScreenSaverView decompressXML:xmld];
1090 if (! desc) goto FAIL;
1092 r = [desc rangeOfString:@"<_description>"
1093 options:NSCaseInsensitiveSearch];
1094 if (r.length == 0) {
1098 desc = [desc substringFromIndex: r.location + r.length];
1099 r = [desc rangeOfString:@"</_description>"
1100 options:NSCaseInsensitiveSearch];
1102 desc = [desc substringToIndex: r.location];
1104 // Leading and trailing whitespace.
1105 desc = [desc stringByTrimmingCharactersInSet:
1106 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1108 // Let's see if we can find a year on the last line.
1109 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
1111 for (NSString *word in
1112 [[desc substringFromIndex:r.location + r.length]
1113 componentsSeparatedByCharactersInSet:
1114 [NSCharacterSet characterSetWithCharactersInString:
1116 int n = [word doubleValue];
1117 if (n > 1970 && n < 2100)
1121 // Delete everything after the first blank line.
1123 r = [desc rangeOfString:@"\n\n" options:0];
1125 desc = [desc substringToIndex: r.location];
1127 // Unwrap lines and compress whitespace.
1129 NSString *result = @"";
1130 for (NSString *s in [desc componentsSeparatedByCharactersInSet:
1131 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
1132 if ([result length] == 0)
1134 else if ([s length] > 0)
1135 result = [NSString stringWithFormat: @"%@ %@", result, s];
1141 desc = [year stringByAppendingString:
1142 [@": " stringByAppendingString: desc]];
1145 desc = year ? year : @"";
1149 if ([saverNames count] > 1)
1150 desc = @"Oops, this module appears to be incomplete.";
1158 - (NSString *) makeDesc:(NSString *)saver
1160 return [self makeDesc:saver yearOnly:NO];
1165 /* Create a dictionary of one-line descriptions of every saver,
1166 for display on the UITableView.
1168 - (NSDictionary *)makeDescTable
1170 NSMutableDictionary *dict =
1171 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1172 for (NSString *saver in saverNames) {
1173 [dict setObject:[self makeDesc:saver] forKey:saver];
1179 - (void) wantsFadeOut:(XScreenSaverView *)sender
1181 rotating_nav.view.hidden = NO; // In case it was hidden during startup.
1183 /* The XScreenSaverView screws with the status bar orientation, mostly to
1184 keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0
1185 and/or 8.2), this confuses the UINavigationController, so put the
1186 orientation back to portrait before dismissing the SaverViewController.
1188 [[UIApplication sharedApplication]
1189 setStatusBarOrientation:UIInterfaceOrientationPortrait
1192 [rotating_nav dismissViewControllerAnimated:YES completion:^() {
1193 [nonrotating_controller release];
1194 nonrotating_controller = nil;
1195 [[rotating_nav view] becomeFirstResponder];
1200 - (void) didShake:(XScreenSaverView *)sender
1202 # if TARGET_IPHONE_SIMULATOR
1203 NSLog (@"simulating shake on saver list");
1205 [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
1210 #endif // USE_IPHONE
1214 /* This is called when the "selectedSaverName" pref changes, e.g.,
1215 when a menu selection is made.
1217 - (void)observeValueForKeyPath:(NSString *)keyPath
1219 change:(NSDictionary *)change
1220 context:(void *)context
1222 SEL dispatchSelector = (SEL)context;
1223 if (dispatchSelector != NULL) {
1224 [self performSelector:dispatchSelector withObject:change];
1226 [super observeValueForKeyPath:keyPath
1236 /* Create the desktop window shell, possibly including a preferences button.
1238 - (NSWindow *) makeWindow
1241 static int count = 0;
1242 Bool simple_p = ([saverNames count] == 1);
1244 NSPopUpButton *menu = 0;
1249 sv_rect.origin.x = sv_rect.origin.y = 0;
1250 sv_rect.size.width = 320;
1251 sv_rect.size.height = 240;
1252 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
1253 initWithFrame:sv_rect
1256 // make a "Preferences" button
1261 rect.size.width = rect.size.height = 10;
1262 pb = [[NSButton alloc] initWithFrame:rect];
1263 [pb setTitle:@"Preferences"];
1264 [pb setBezelStyle:NSRoundedBezelStyle];
1267 rect.origin.x = ([sv frame].size.width -
1268 [pb frame].size.width) / 2;
1269 [pb setFrameOrigin:rect.origin];
1273 [pb setTarget:self];
1274 [pb setAction:@selector(openPreferences:)];
1276 // Make a saver selection menu
1278 menu = [self makeMenu];
1281 [menu setFrameOrigin:rect.origin];
1283 // make a box to wrap the saverView
1287 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1288 gbox = [[NSBox alloc] initWithFrame:rect];
1289 rect.size.width = rect.size.height = 10;
1290 [gbox setContentViewMargins:rect.size];
1291 [gbox setTitlePosition:NSNoTitle];
1292 [gbox addSubview:sv];
1295 // make a box to wrap the other two boxes
1297 rect.origin.x = rect.origin.y = 0;
1298 rect.size.width = [gbox frame].size.width;
1299 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1300 pbox = [[NSBox alloc] initWithFrame:rect];
1301 [pbox setTitlePosition:NSNoTitle];
1302 [pbox setBorderType:NSNoBorder];
1303 [pbox addSubview:gbox];
1304 if (menu) [pbox addSubview:menu];
1305 if (pb) [pbox addSubview:pb];
1308 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1309 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1310 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1311 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1314 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1317 // and make a window to hold that.
1319 NSScreen *screen = [NSScreen mainScreen];
1320 rect = pbox ? [pbox frame] : [sv frame];
1321 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1322 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1324 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1326 NSWindow *win = [[NSWindow alloc]
1327 initWithContentRect:rect
1328 styleMask:(NSTitledWindowMask |
1329 NSClosableWindowMask |
1330 NSMiniaturizableWindowMask |
1331 NSResizableWindowMask)
1332 backing:NSBackingStoreBuffered
1335 [win setMinSize:[win frameRectForContentRect:rect].size];
1336 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1338 [win makeKeyAndOrderFront:win];
1340 [sv startAnimation]; // this is the dummy saver
1350 for (NSWindow *win in windows) {
1351 ScreenSaverView *sv = find_saverView ([win contentView]);
1352 if ([sv isAnimating])
1353 [sv animateOneFrame];
1357 # endif // !USE_IPHONE
1360 - (void)applicationDidFinishLaunching:
1362 (NSNotification *) notif
1363 # else // USE_IPHONE
1364 (UIApplication *) application
1365 # endif // USE_IPHONE
1367 [self listSaverBundleNames];
1369 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1372 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1373 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1378 // Create either one window (for standalone, e.g. Phosphor.app)
1379 // or two windows for SaverTester.app.
1380 for (i = 0; i < window_count; i++) {
1381 NSWindow *win = [self makeWindow];
1382 // Get the last-saved window position out of preferences.
1383 [win setFrameAutosaveName:
1384 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1385 [win setFrameUsingName:[win frameAutosaveName]];
1387 // This prevents clicks from being seen by savers.
1388 // [win setMovableByWindowBackground:YES];
1390 # else // USE_IPHONE
1392 # undef ya_rand_init
1393 ya_rand_init (0); // Now's a good time.
1397 "You must call this method before attempting to get orientation data from
1398 the receiver. This method enables the device's accelerometer hardware
1399 and begins the delivery of acceleration events to the receiver."
1401 Adding or removing this doesn't seem to make any difference. It's
1402 probably getting called by the UINavigationController. Still... */
1403 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
1405 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1408 if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
1409 rotating_nav.view.hidden = YES;
1411 [window setRootViewController: rotating_nav];
1412 [window setAutoresizesSubviews:YES];
1413 [window setAutoresizingMask:
1414 (UIViewAutoresizingFlexibleWidth |
1415 UIViewAutoresizingFlexibleHeight)];
1417 SaverListController *menu = [[SaverListController alloc]
1418 initWithNames:saverNames
1419 descriptions:[self makeDescTable]];
1420 [rotating_nav pushViewController:menu animated:YES];
1421 [menu becomeFirstResponder];
1423 application.applicationSupportsShakeToEdit = YES;
1426 # endif // USE_IPHONE
1428 NSString *forced = 0;
1429 /* In the XCode project, each .saver scheme sets this env var when
1430 launching SaverTester.app so that it knows which one we are
1431 currently debugging. If this is set, it overrides the default
1432 selection in the popup menu. If unset, that menu persists to
1433 whatever it was last time.
1435 const char *f = getenv ("SELECTED_SAVER");
1437 forced = [NSString stringWithCString:(char *)f
1438 encoding:NSUTF8StringEncoding];
1440 if (forced && ![saverNames containsObject:forced]) {
1441 NSLog(@"forced saver \"%@\" does not exist", forced);
1445 // If there's only one saver, run that.
1446 if (!forced && [saverNames count] == 1)
1447 forced = [saverNames objectAtIndex:0];
1450 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1455 // If nothing was selected (e.g., this is the first launch)
1456 // then scroll randomly instead of starting up at "A".
1459 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1462 [menu scrollTo: prev];
1463 # endif // USE_IPHONE
1466 [prefs setObject:forced forKey:@"selectedSaverName"];
1469 /* Don't auto-launch the saver unless it was running last time.
1470 XScreenSaverView manages this, on crash_timer.
1473 if (!forced && ![prefs boolForKey:@"wasRunning"])
1477 [self selectedSaverDidChange:nil];
1478 // [NSTimer scheduledTimerWithTimeInterval: 0
1480 // selector:@selector(selectedSaverDidChange:)
1487 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1488 ScreenSaverView to run its own timer calling animateOneFrame.
1489 On 10.9, that fails because the private class ScreenSaverModule
1490 is only initialized properly by ScreenSaverEngine, and in the
1491 context of SaverRunner, the null ScreenSaverEngine instance
1492 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1493 So, if it looks like this is the 10.9 version of ScreenSaverModule
1494 instead of the 10.8 version, we run our own timer here. This sucks.
1497 Class ssm = NSClassFromString (@"ScreenSaverModule");
1498 if (ssm && [ssm instancesRespondToSelector:
1499 @selector(needsAnimationTimer)]) {
1500 NSWindow *win = [windows objectAtIndex:0];
1501 ScreenSaverView *sv = find_saverView ([win contentView]);
1502 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1503 [sv animationTimeInterval]
1505 selector:@selector(animTimer)
1510 # endif // !USE_IPHONE
1516 /* When the window closes, exit (even if prefs still open.)
1518 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1523 # else // USE_IPHONE
1525 - (void)applicationWillResignActive:(UIApplication *)app
1527 [(XScreenSaverView *)view setScreenLocked:YES];
1530 - (void)applicationDidBecomeActive:(UIApplication *)app
1532 [(XScreenSaverView *)view setScreenLocked:NO];
1535 - (void)applicationDidEnterBackground:(UIApplication *)application
1537 [(XScreenSaverView *)view setScreenLocked:YES];
1540 #endif // USE_IPHONE