1 /* xscreensaver, Copyright (c) 2006-2016 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 - (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */
82 return UIInterfaceOrientationMaskAll;
88 @implementation SaverViewController
90 @synthesize saverName;
92 - (id)initWithSaverRunner:(SaverRunner *)parent
93 showAboutBox:(BOOL)showAboutBox
98 // _storedOrientation = UIInterfaceOrientationUnknown;
99 _showAboutBox = showAboutBox;
101 self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
102 self.wantsFullScreenLayout = YES;
110 [_saverName release];
111 // iOS: When a UIView deallocs, it doesn't do [UIView removeFromSuperView]
112 // for its subviews, so the subviews end up with a dangling pointer in their
113 // superview properties.
114 [aboutBox removeFromSuperview];
116 [_saverView removeFromSuperview];
117 [_saverView release];
124 // The UIViewController's view must never change, so it gets set here to
125 // a plain black background.
127 // This background view doesn't block the status bar, but that's probably
128 // OK, because it's never on screen for more than a fraction of a second.
129 UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull];
130 backgroundView.backgroundColor = [UIColor blackColor];
131 self.view = backgroundView;
132 [backgroundView release];
136 - (void)aboutPanel:(UIView *)saverView
137 orientation:(UIInterfaceOrientation)orient
142 NSString *name = _saverName;
143 NSString *year = [_parent makeDesc:_saverName yearOnly:YES];
146 CGRect frame = [saverView frame];
150 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
151 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
154 CGSize s = CGSizeMake(frame.size.width, frame.size.height);
155 CGSize tsize1 = [[[NSAttributedString alloc]
157 attributes:@{ NSFontAttributeName: font1 }]
158 boundingRectWithSize: s
159 options: NSStringDrawingUsesLineFragmentOrigin
161 CGSize tsize2 = [[[NSAttributedString alloc]
163 attributes:@{ NSFontAttributeName: font2 }]
164 boundingRectWithSize: s
165 options: NSStringDrawingUsesLineFragmentOrigin
167 # else // iOS 6 or Cocoa
168 CGSize tsize1 = [name sizeWithFont:font1
169 constrainedToSize:CGSizeMake(frame.size.width,
171 CGSize tsize2 = [year sizeWithFont:font2
172 constrainedToSize:CGSizeMake(frame.size.width,
176 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
177 tsize1.width : tsize2.width,
178 tsize1.height + tsize2.height);
180 tsize.width = ceilf(tsize.width);
181 tsize.height = ceilf(tsize.height);
183 // Don't know how to find inner margin of UITextView.
185 tsize.width += margin * 4;
186 tsize.height += margin * 2;
188 if ([saverView frame].size.width >= 768)
189 tsize.height += pt1 * 3; // extra bottom margin on iPad
191 frame = CGRectMake (0, 0, tsize.width, tsize.height);
193 /* Get the text oriented properly, and move it to the bottom of the
194 screen, since many savers have action in the middle.
197 case UIInterfaceOrientationLandscapeLeft:
199 frame.origin.x = ([saverView frame].size.width
200 - (tsize.width - tsize.height) / 2
202 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
204 case UIInterfaceOrientationLandscapeRight:
206 frame.origin.x = -(tsize.width - tsize.height) / 2;
207 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
209 case UIInterfaceOrientationPortraitUpsideDown:
211 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
216 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
217 frame.origin.y = [saverView frame].size.height - tsize.height;
222 [aboutBox removeFromSuperview];
226 aboutBox = [[UIView alloc] initWithFrame:frame];
228 aboutBox.transform = CGAffineTransformMakeRotation (rot);
229 aboutBox.backgroundColor = [UIColor clearColor];
231 /* There seems to be no easy way to stroke the font, so instead draw
232 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
233 a black shadow to each. (You'd think the shadow alone would be
234 enough, but there's no way to make it dark enough to be legible.)
236 for (int i = 0; i < 5; i++) {
237 UITextView *textview;
239 frame.origin.x = frame.origin.y = 0;
241 case 0: frame.origin.x = -off; break;
242 case 1: frame.origin.x = off; break;
243 case 2: frame.origin.y = -off; break;
244 case 3: frame.origin.y = off; break;
247 for (int j = 0; j < 2; j++) {
249 frame.origin.y = (j == 0 ? 0 : pt1);
250 textview = [[UITextView alloc] initWithFrame:frame];
251 textview.font = (j == 0 ? font1 : font2);
252 textview.text = (j == 0 ? name : year);
253 textview.textAlignment = NSTextAlignmentCenter;
254 textview.showsHorizontalScrollIndicator = NO;
255 textview.showsVerticalScrollIndicator = NO;
256 textview.scrollEnabled = NO;
257 textview.editable = NO;
258 textview.userInteractionEnabled = NO;
259 textview.backgroundColor = [UIColor clearColor];
260 textview.textColor = (i == 4
261 ? [UIColor yellowColor]
262 : [UIColor blackColor]);
264 CALayer *textLayer = (CALayer *)
265 [textview.layer.sublayers objectAtIndex:0];
266 textLayer.shadowColor = [UIColor blackColor].CGColor;
267 textLayer.shadowOffset = CGSizeMake(0, 0);
268 textLayer.shadowOpacity = 1;
269 textLayer.shadowRadius = 2;
271 [aboutBox addSubview:textview];
275 CABasicAnimation *anim =
276 [CABasicAnimation animationWithKeyPath:@"opacity"];
278 anim.repeatCount = 1;
279 anim.autoreverses = NO;
280 anim.fromValue = [NSNumber numberWithFloat:0.0];
281 anim.toValue = [NSNumber numberWithFloat:1.0];
282 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
284 [saverView addSubview:aboutBox];
287 [splashTimer invalidate];
290 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
292 selector:@selector(aboutOff)
300 [self aboutOff:FALSE];
303 - (void)aboutOff:(BOOL)fast
307 [splashTimer invalidate];
311 aboutBox.layer.opacity = 0;
315 CABasicAnimation *anim =
316 [CABasicAnimation animationWithKeyPath:@"opacity"];
318 anim.repeatCount = 1;
319 anim.autoreverses = NO;
320 anim.fromValue = [NSNumber numberWithFloat: 1];
321 anim.toValue = [NSNumber numberWithFloat: 0];
322 // anim.delegate = self;
323 aboutBox.layer.opacity = 0;
324 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
329 - (void)createSaverView
331 UIView *parentView = self.view;
334 [_saverView removeFromSuperview];
335 [_saverView release];
339 if (_storedOrientation != UIInterfaceOrientationUnknown) {
340 [[UIApplication sharedApplication]
341 setStatusBarOrientation:_storedOrientation
346 _saverView = [_parent newSaverView:_saverName
347 withSize:parentView.bounds.size];
350 [[[UIAlertView alloc] initWithTitle: _saverName
351 message: @"Unable to load!"
353 cancelButtonTitle: @"Bummer"
354 otherButtonTitles: nil]
359 _saverView.delegate = _parent;
360 _saverView.autoresizingMask =
361 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
363 [self.view addSubview:_saverView];
365 // The first responder must be set only after the view was placed in the view
367 [_saverView becomeFirstResponder]; // For shakes on iOS 6.
368 [_saverView startAnimation];
369 [self aboutPanel:_saverView
370 orientation:/* _storedOrientation */ UIInterfaceOrientationPortrait];
374 - (void)viewDidAppear:(BOOL)animated
376 [super viewDidAppear:animated];
377 [self createSaverView];
381 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
383 return NO; /* Deprecated in iOS 6 */
387 - (BOOL)shouldAutorotate /* Added in iOS 6 */
393 - (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */
395 // Lies from the iOS docs:
396 // "This method is only called if the view controller's shouldAutorotate
397 // method returns YES."
398 return UIInterfaceOrientationMaskAll;
403 - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
405 return UIInterfaceOrientationPortrait;
410 - (void)setSaverName:(NSString *)name
413 [_saverName release];
415 // _storedOrientation =
416 // [UIApplication sharedApplication].statusBarOrientation;
419 [self createSaverView];
423 - (void)viewWillTransitionToSize: (CGSize)size
424 withTransitionCoordinator:
425 (id<UIViewControllerTransitionCoordinator>) coordinator
427 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
432 [CATransaction begin];
434 // Completely suppress the rotation animation, since we
435 // will not (visually) be rotating at all.
436 if ([_saverView suppressRotationAnimation])
437 [CATransaction setDisableActions:YES];
439 [self aboutOff:TRUE]; // It does goofy things if we rotate while it's up
441 [coordinator animateAlongsideTransition:^
442 (id <UIViewControllerTransitionCoordinatorContext> context) {
443 // This executes repeatedly during the rotation.
444 } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
445 // This executes once when the rotation has finished.
446 [CATransaction commit];
447 [_saverView orientationChanged];
449 // No code goes here, as it would execute before the above completes.
457 @implementation SaverRunner
460 - (XScreenSaverView *) newSaverView: (NSString *) module
461 withSize: (NSSize) size
467 // Load the XScreenSaverView subclass and code from a ".saver" bundle.
469 NSString *name = [module stringByAppendingPathExtension:@"saver"];
470 NSString *path = [saverDir stringByAppendingPathComponent:name];
472 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
473 NSLog(@"bundle \"%@\" does not exist", path);
477 NSLog(@"Loading %@", path);
479 // NSBundle *obundle = saverBundle;
481 saverBundle = [NSBundle bundleWithPath:path];
483 new_class = [saverBundle principalClass];
485 // Not entirely unsurprisingly, this tends to break the world.
486 // if (obundle && obundle != saverBundle)
491 // Determine whether to create an X11 view or an OpenGL view by
492 // looking for the "gl" tag in the xml file. This is kind of awful.
494 NSString *path = [saverDir
495 stringByAppendingPathComponent:
496 [[[module lowercaseString]
497 stringByReplacingOccurrencesOfString:@" "
499 stringByAppendingPathExtension:@"xml"]];
500 NSData *xmld = [NSData dataWithContentsOfFile:path];
501 NSAssert (xmld, @"no XML: %@", path);
502 NSString *xml = [XScreenSaverView decompressXML:xmld];
503 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
506 ? [XScreenSaverGLView class]
507 : [XScreenSaverView class]);
509 # endif // USE_IPHONE
515 rect.origin.x = rect.origin.y = 0;
516 rect.size.width = size.width;
517 rect.size.height = size.height;
519 XScreenSaverView *instance =
520 [(XScreenSaverView *) [new_class alloc]
525 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
530 /* KLUGE: Inform the underlying program that we're in "standalone"
531 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
532 This is kind of horrible but I haven't thought of a more sensible
533 way to make this work.
536 if ([saverNames count] == 1) {
537 setenv ("XSCREENSAVER_STANDALONE", "1", 1);
541 return (XScreenSaverView *) instance;
547 static ScreenSaverView *
548 find_saverView_child (NSView *v)
550 NSArray *kids = [v subviews];
551 int nkids = [kids count];
553 for (i = 0; i < nkids; i++) {
554 NSObject *kid = [kids objectAtIndex:i];
555 if ([kid isKindOfClass:[ScreenSaverView class]]) {
556 return (ScreenSaverView *) kid;
558 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
566 static ScreenSaverView *
567 find_saverView (NSView *v)
570 NSView *p = [v superview];
574 return find_saverView_child (v);
578 /* Changes the contents of the menubar menus to correspond to
579 the running saver. Desktop only.
582 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
584 if ([v isKindOfClass:[NSMenu class]]) {
585 NSMenu *m = (NSMenu *)v;
586 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
587 withString:new_str]];
588 NSArray *kids = [m itemArray];
589 int nkids = [kids count];
591 for (i = 0; i < nkids; i++) {
592 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
594 } else if ([v isKindOfClass:[NSMenuItem class]]) {
595 NSMenuItem *mi = (NSMenuItem *)v;
596 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
597 withString:new_str]];
598 NSMenu *m = [mi submenu];
599 if (m) relabel_menus (m, old_str, new_str);
604 - (void) openPreferences: (id) sender
607 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
608 sv = find_saverView ((NSView *) sender);
612 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
613 w = [windows objectAtIndex:i];
614 if ([w isKeyWindow]) break;
616 sv = find_saverView ([w contentView]);
619 NSAssert (sv, @"no saver view");
621 NSWindow *prefs = [sv configureSheet];
623 [NSApp beginSheet:prefs
624 modalForWindow:[sv window]
626 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
628 int code = [NSApp runModalForWindow:prefs];
630 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
631 We have to restart *both* animations, because the xlockmore-style
632 ones will blow up if one re-inits but the other doesn't.
634 if (code != NSCancelButton) {
635 if ([sv isAnimating])
642 - (void) preferencesClosed: (NSWindow *) sheet
643 returnCode: (int) returnCode
644 contextInfo: (void *) contextInfo
646 [NSApp stopModalWithCode:returnCode];
652 - (UIImage *) screenshot
654 return saved_screenshot;
657 - (void) saveScreenshot
659 // Most of this is from:
660 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
661 // The rotation stuff is by me.
663 CGSize size = [[UIScreen mainScreen] bounds].size;
665 // iOS 7: Needs to be the actual device orientation.
666 // iOS 8: Needs to be UIInterfaceOrientationPortrait.
668 UIInterfaceOrientation orient =
669 NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 ?
670 UIInterfaceOrientationPortrait /* iOS 8 broke -[UIScreen bounds]. */ :
671 [[window rootViewController] interfaceOrientation];
673 if (orient == UIInterfaceOrientationLandscapeLeft ||
674 orient == UIInterfaceOrientationLandscapeRight) {
675 // Rotate the shape of the canvas 90 degrees.
676 double s = size.width;
677 size.width = size.height;
682 // Create a graphics context with the target size
683 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
684 // take the scale into consideration
685 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
687 if (UIGraphicsBeginImageContextWithOptions)
688 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
690 UIGraphicsBeginImageContext (size);
692 CGContextRef ctx = UIGraphicsGetCurrentContext();
695 // Rotate the graphics context to match current hardware rotation.
698 case UIInterfaceOrientationPortraitUpsideDown:
699 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
700 CGContextRotateCTM (ctx, M_PI);
701 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
703 case UIInterfaceOrientationLandscapeLeft:
704 case UIInterfaceOrientationLandscapeRight:
705 CGContextTranslateCTM (ctx,
706 ([window frame].size.height -
707 [window frame].size.width) / 2,
708 ([window frame].size.width -
709 [window frame].size.height) / 2);
710 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
711 CGContextRotateCTM (ctx,
712 (orient == UIInterfaceOrientationLandscapeLeft
715 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
721 // Iterate over every window from back to front
723 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
724 if (![win respondsToSelector:@selector(screen)] ||
725 [win screen] == [UIScreen mainScreen]) {
727 // -renderInContext: renders in the coordinate space of the layer,
728 // so we must first apply the layer's geometry to the graphics context
729 CGContextSaveGState (ctx);
731 // Center the context around the window's anchor point
732 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
734 // Apply the window's transform about the anchor point
735 CGContextConcatCTM (ctx, [win transform]);
737 // Offset by the portion of the bounds left of and above anchor point
738 CGContextTranslateCTM (ctx,
739 -[win bounds].size.width * [[win layer] anchorPoint].x,
740 -[win bounds].size.height * [[win layer] anchorPoint].y);
742 // Render the layer hierarchy to the current context
743 [[win layer] renderInContext:ctx];
745 // Restore the context
746 CGContextRestoreGState (ctx);
750 if (saved_screenshot)
751 [saved_screenshot release];
752 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
754 UIGraphicsEndImageContext();
758 - (void) openPreferences: (NSString *) saver
760 XScreenSaverView *saverView = [self newSaverView:saver
761 withSize:CGSizeMake(0, 0)];
762 if (! saverView) return;
764 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
765 [prefs setObject:saver forKey:@"selectedSaverName"];
768 [rotating_nav pushViewController: [saverView configureView]
777 - (void)loadSaver:(NSString *)name
781 if (saverName && [saverName isEqualToString: name]) {
782 for (NSWindow *win in windows) {
783 ScreenSaverView *sv = find_saverView ([win contentView]);
784 if (![sv isAnimating])
792 for (NSWindow *win in windows) {
793 NSView *cv = [win contentView];
794 NSString *old_title = [win title];
795 if (!old_title) old_title = @"XScreenSaver";
796 [win setTitle: name];
797 relabel_menus (menubar, old_title, name);
799 ScreenSaverView *old_view = find_saverView (cv);
800 NSView *sup = old_view ? [old_view superview] : cv;
803 if ([old_view isAnimating])
804 [old_view stopAnimation];
805 [old_view removeFromSuperview];
808 NSSize size = [cv frame].size;
809 ScreenSaverView *new_view = [self newSaverView:name withSize: size];
810 NSAssert (new_view, @"unable to make a saver view");
812 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
813 [sup addSubview: new_view];
814 [win makeFirstResponder:new_view];
815 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
816 [new_view startAnimation];
820 NSUserDefaultsController *ctl =
821 [NSUserDefaultsController sharedUserDefaultsController];
826 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
827 NSLog (@"selecting saver \"%@\"", name);
830 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
831 [prefs setObject:name forKey:@"selectedSaverName"];
834 /* Cacheing this screws up rotation when starting a saver twice in a row.
835 if (saverName && [saverName isEqualToString: name]) {
836 if ([saverView isAnimating])
845 if (nonrotating_controller) {
846 nonrotating_controller.saverName = name;
850 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
851 UIScreen *screen = [UIScreen mainScreen];
853 /* 'nativeScale' is very confusing.
856 bounds: 320x480 scale: 2
857 nativeBounds: 640x960 nativeScale: 2
859 bounds: 320x568 scale: 2
860 nativeBounds: 640x1136 nativeScale: 2
862 bounds: 768x1024 scale: 1
863 nativeBounds: 768x1024 nativeScale: 1
865 bounds: 768x1024 scale: 2
866 nativeBounds: 1536x2048 nativeScale: 2
868 bounds: 320x568 scale: 2
869 nativeBounds: 640x1136 nativeScale: 2
871 bounds: 320x568 scale: 2
872 nativeBounds: 960x1704 nativeScale: 3
874 According to a StackOverflow comment:
876 The iPhone 6+ renders internally using @3x assets at a virtual
877 resolution of 2208x1242 (with 736x414 points), then samples that down
878 for display. The same as using a scaled resolution on a Retina MacBook
879 -- it lets them hit an integral multiple for pixel assets while still
880 having e.g. 12pt text look the same size on the screen.
882 The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
883 and use @2x assets to stick to the approximately 160 points per inch
884 of all previous devices.
886 The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
887 @2.46x assets. Instead Apple uses @3x assets and scales the complete
888 output down to about 84% of its natural size.
890 In practice Apple has decided to go with more like 87%, turning the
891 1080 into 1242. No doubt that was to find something as close as
892 possible to 84% that still produced integral sizes in both directions
893 -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
894 into, say, 1286, you'd somehow need to render 2286.22 pixels
895 vertically to scale well.
898 NSLog(@"screen: %.0fx%0.f",
899 [[screen currentMode] size].width,
900 [[screen currentMode] size].height);
901 NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
902 [screen bounds].size.width,
903 [screen bounds].size.height,
905 [screen scale] * [screen bounds].size.width,
906 [screen scale] * [screen bounds].size.height);
909 if ([screen respondsToSelector:@selector(nativeBounds)])
910 NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
911 [screen nativeBounds].size.width,
912 [screen nativeBounds].size.height,
913 [screen nativeScale],
914 [screen nativeBounds].size.width / [screen nativeScale],
915 [screen nativeBounds].size.height / [screen nativeScale]);
917 # endif // TARGET_IPHONE_SIMULATOR
919 // Take the screen shot before creating the screen saver view, because this
920 // can screw with the layout.
921 [self saveScreenshot];
923 // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal
924 // presentation full screen.
925 rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen;
927 nonrotating_controller = [[SaverViewController alloc]
928 initWithSaverRunner:self
929 showAboutBox:[saverNames count] != 1];
930 nonrotating_controller.saverName = name;
934 [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil];
936 // Doing this makes savers cut back to the list instead of fading,
937 // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
938 // [window setHidden:YES];
940 # endif // USE_IPHONE
946 - (void)aboutPanel:(id)sender
948 NSDictionary *bd = [saverBundle infoDictionary];
949 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
951 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
952 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
953 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
954 forKey:@"ApplicationVersion"];
955 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
956 NSAttributedString *s = [[NSAttributedString alloc]
957 initWithString: (NSString *)
958 [bd objectForKey:@"CFBundleGetInfoString"]];
959 [d setValue:s forKey:@"Credits"];
962 [[NSApplication sharedApplication]
963 orderFrontStandardAboutPanelWithOptions:d];
966 #endif // !USE_IPHONE
970 - (void)selectedSaverDidChange:(NSDictionary *)change
972 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
973 NSString *name = [prefs stringForKey:@"selectedSaverName"];
977 if (! [saverNames containsObject:name]) {
978 NSLog (@"saver \"%@\" does not exist", name);
982 [self loadSaver: name];
986 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
989 NSString *ext = @"saver";
991 NSString *ext = @"xml";
994 NSArray *files = [[NSFileManager defaultManager]
995 contentsOfDirectoryAtPath:dir error:nil];
996 if (! files) return 0;
997 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
999 for (NSString *p in files) {
1000 if ([[p pathExtension] caseInsensitiveCompare: ext])
1003 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
1006 // Get the saver name's capitalization right by reading the XML file.
1008 p = [dir stringByAppendingPathComponent: p];
1009 NSData *xmld = [NSData dataWithContentsOfFile:p];
1010 NSAssert (xmld, @"no XML: %@", p);
1011 NSString *xml = [XScreenSaverView decompressXML:xmld];
1012 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
1013 NSAssert1 (r.length, @"no name in %@", p);
1015 xml = [xml substringFromIndex: r.location + r.length];
1016 r = [xml rangeOfString:@"\"" options:0];
1017 if (r.length) name = [xml substringToIndex: r.location];
1020 # endif // USE_IPHONE
1022 NSAssert1 (name, @"no name in %@", p);
1023 if (name) [result addObject: name];
1026 if (! [result count])
1034 - (NSArray *) listSaverBundleNames
1036 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
1039 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
1040 // directories in the bundle.
1041 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
1042 stringByAppendingPathComponent:@"Contents"]
1043 stringByAppendingPathComponent:@"Resources"]];
1044 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
1046 // Also look in the same directory as the executable.
1047 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
1048 stringByDeletingLastPathComponent]];
1050 // Finally, look in standard MacOS screensaver directories.
1051 // [dirs addObject: @"~/Library/Screen Savers"];
1052 // [dirs addObject: @"/Library/Screen Savers"];
1053 // [dirs addObject: @"/System/Library/Screen Savers"];
1055 # else // USE_IPHONE
1057 // On iOS, only look in the bundle's root directory.
1058 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
1060 # endif // USE_IPHONE
1063 for (i = 0; i < [dirs count]; i++) {
1064 NSString *dir = [dirs objectAtIndex:i];
1065 NSArray *names = [self listSaverBundleNamesInDir:dir];
1066 if (! names) continue;
1067 saverDir = [dir retain];
1068 saverNames = [names retain];
1072 NSString *err = @"no .saver bundles found in: ";
1073 for (i = 0; i < [dirs count]; i++) {
1074 if (i) err = [err stringByAppendingString:@", "];
1075 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
1076 stringByAbbreviatingWithTildeInPath]];
1077 err = [err stringByAppendingString:@"/"];
1080 return [NSArray array];
1084 /* Create the popup menu of available saver names.
1088 - (NSPopUpButton *) makeMenu
1091 rect.origin.x = rect.origin.y = 0;
1092 rect.size.width = 10;
1093 rect.size.height = 10;
1094 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1097 float max_width = 0;
1098 for (i = 0; i < [saverNames count]; i++) {
1099 NSString *name = [saverNames objectAtIndex:i];
1100 [popup addItemWithTitle:name];
1101 [[popup itemWithTitle:name] setRepresentedObject:name];
1103 NSRect r = [popup frame];
1104 if (r.size.width > max_width) max_width = r.size.width;
1107 // Bind the menu to preferences, and trigger a callback when an item
1110 NSString *key = @"values.selectedSaverName";
1111 NSUserDefaultsController *prefs =
1112 [NSUserDefaultsController sharedUserDefaultsController];
1113 [prefs addObserver:self
1116 context:@selector(selectedSaverDidChange:)];
1117 [popup bind:@"selectedObject"
1121 [prefs setAppliesImmediately:YES];
1123 NSRect r = [popup frame];
1124 r.size.width = max_width;
1126 [popup autorelease];
1132 - (NSString *) makeDesc:(NSString *)saver
1133 yearOnly:(BOOL) yearp
1136 NSString *path = [saverDir stringByAppendingPathComponent:
1137 [[saver lowercaseString]
1138 stringByReplacingOccurrencesOfString:@" "
1142 path = [path stringByAppendingPathExtension:@"xml"];
1143 NSData *xmld = [NSData dataWithContentsOfFile:path];
1144 if (! xmld) goto FAIL;
1145 desc = [XScreenSaverView decompressXML:xmld];
1146 if (! desc) goto FAIL;
1148 r = [desc rangeOfString:@"<_description>"
1149 options:NSCaseInsensitiveSearch];
1150 if (r.length == 0) {
1154 desc = [desc substringFromIndex: r.location + r.length];
1155 r = [desc rangeOfString:@"</_description>"
1156 options:NSCaseInsensitiveSearch];
1158 desc = [desc substringToIndex: r.location];
1160 // Leading and trailing whitespace.
1161 desc = [desc stringByTrimmingCharactersInSet:
1162 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1164 // Let's see if we can find a year on the last line.
1165 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
1167 for (NSString *word in
1168 [[desc substringFromIndex:r.location + r.length]
1169 componentsSeparatedByCharactersInSet:
1170 [NSCharacterSet characterSetWithCharactersInString:
1172 int n = [word doubleValue];
1173 if (n > 1970 && n < 2100)
1177 // Delete everything after the first blank line.
1179 r = [desc rangeOfString:@"\n\n" options:0];
1181 desc = [desc substringToIndex: r.location];
1183 // Unwrap lines and compress whitespace.
1185 NSString *result = @"";
1186 for (NSString *s in [desc componentsSeparatedByCharactersInSet:
1187 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
1188 if ([result length] == 0)
1190 else if ([s length] > 0)
1191 result = [NSString stringWithFormat: @"%@ %@", result, s];
1197 desc = [year stringByAppendingString:
1198 [@": " stringByAppendingString: desc]];
1201 desc = year ? year : @"";
1205 if ([saverNames count] > 1)
1206 desc = @"Oops, this module appears to be incomplete.";
1214 - (NSString *) makeDesc:(NSString *)saver
1216 return [self makeDesc:saver yearOnly:NO];
1221 /* Create a dictionary of one-line descriptions of every saver,
1222 for display on the UITableView.
1224 - (NSDictionary *)makeDescTable
1226 NSMutableDictionary *dict =
1227 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1228 for (NSString *saver in saverNames) {
1229 [dict setObject:[self makeDesc:saver] forKey:saver];
1235 - (void) wantsFadeOut:(XScreenSaverView *)sender
1237 rotating_nav.view.hidden = NO; // In case it was hidden during startup.
1239 /* The XScreenSaverView screws with the status bar orientation, mostly to
1240 keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0
1241 and/or 8.2), this confuses the UINavigationController, so put the
1242 orientation back to portrait before dismissing the SaverViewController.
1245 [[UIApplication sharedApplication]
1246 setStatusBarOrientation:UIInterfaceOrientationPortrait
1250 /* Make sure the most-recently-run saver is visible. Sometimes it ends
1251 up scrolled half a line off the bottom of the screen.
1254 for (UIViewController *v in [rotating_nav viewControllers]) {
1255 if ([v isKindOfClass:[SaverListController class]]) {
1256 [(SaverListController *)v scrollTo: saverName];
1262 [rotating_nav dismissViewControllerAnimated:YES completion:^() {
1263 [nonrotating_controller release];
1264 nonrotating_controller = nil;
1265 [[rotating_nav view] becomeFirstResponder];
1270 - (void) didShake:(XScreenSaverView *)sender
1272 # if TARGET_IPHONE_SIMULATOR
1273 NSLog (@"simulating shake on saver list");
1275 [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
1280 #endif // USE_IPHONE
1284 /* This is called when the "selectedSaverName" pref changes, e.g.,
1285 when a menu selection is made.
1287 - (void)observeValueForKeyPath:(NSString *)keyPath
1289 change:(NSDictionary *)change
1290 context:(void *)context
1292 SEL dispatchSelector = (SEL)context;
1293 if (dispatchSelector != NULL) {
1294 [self performSelector:dispatchSelector withObject:change];
1296 [super observeValueForKeyPath:keyPath
1306 /* Create the desktop window shell, possibly including a preferences button.
1308 - (NSWindow *) makeWindow
1311 static int count = 0;
1312 Bool simple_p = ([saverNames count] == 1);
1314 NSPopUpButton *menu = 0;
1319 sv_rect.origin.x = sv_rect.origin.y = 0;
1320 sv_rect.size.width = 320;
1321 sv_rect.size.height = 240;
1322 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
1323 initWithFrame:sv_rect
1326 // make a "Preferences" button
1331 rect.size.width = rect.size.height = 10;
1332 pb = [[NSButton alloc] initWithFrame:rect];
1333 [pb setTitle:@"Preferences"];
1334 [pb setBezelStyle:NSRoundedBezelStyle];
1337 rect.origin.x = ([sv frame].size.width -
1338 [pb frame].size.width) / 2;
1339 [pb setFrameOrigin:rect.origin];
1343 [pb setTarget:self];
1344 [pb setAction:@selector(openPreferences:)];
1346 // Make a saver selection menu
1348 menu = [self makeMenu];
1351 [menu setFrameOrigin:rect.origin];
1353 // make a box to wrap the saverView
1357 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1358 gbox = [[NSBox alloc] initWithFrame:rect];
1359 rect.size.width = rect.size.height = 10;
1360 [gbox setContentViewMargins:rect.size];
1361 [gbox setTitlePosition:NSNoTitle];
1362 [gbox addSubview:sv];
1365 // make a box to wrap the other two boxes
1367 rect.origin.x = rect.origin.y = 0;
1368 rect.size.width = [gbox frame].size.width;
1369 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1370 pbox = [[NSBox alloc] initWithFrame:rect];
1371 [pbox setTitlePosition:NSNoTitle];
1372 [pbox setBorderType:NSNoBorder];
1373 [pbox addSubview:gbox];
1375 if (menu) [pbox addSubview:menu];
1376 if (pb) [pbox addSubview:pb];
1380 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1381 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1382 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1383 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1386 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1389 // and make a window to hold that.
1391 NSScreen *screen = [NSScreen mainScreen];
1392 rect = pbox ? [pbox frame] : [sv frame];
1393 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1394 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1396 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1398 NSWindow *win = [[NSWindow alloc]
1399 initWithContentRect:rect
1400 styleMask:(NSTitledWindowMask |
1401 NSClosableWindowMask |
1402 NSMiniaturizableWindowMask |
1403 NSResizableWindowMask)
1404 backing:NSBackingStoreBuffered
1407 [win setMinSize:[win frameRectForContentRect:rect].size];
1408 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1411 [win makeKeyAndOrderFront:win];
1413 [sv startAnimation]; // this is the dummy saver
1424 for (NSWindow *win in windows) {
1425 ScreenSaverView *sv = find_saverView ([win contentView]);
1426 if ([sv isAnimating])
1427 [sv animateOneFrame];
1431 # endif // !USE_IPHONE
1434 - (void)applicationDidFinishLaunching:
1436 (NSNotification *) notif
1437 # else // USE_IPHONE
1438 (UIApplication *) application
1439 # endif // USE_IPHONE
1441 [self listSaverBundleNames];
1443 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1446 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1447 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1452 // Create either one window (for standalone, e.g. Phosphor.app)
1453 // or two windows for SaverTester.app.
1454 for (i = 0; i < window_count; i++) {
1455 NSWindow *win = [self makeWindow];
1456 // Get the last-saved window position out of preferences.
1457 [win setFrameAutosaveName:
1458 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1459 [win setFrameUsingName:[win frameAutosaveName]];
1461 // This prevents clicks from being seen by savers.
1462 // [win setMovableByWindowBackground:YES];
1465 # else // USE_IPHONE
1467 # undef ya_rand_init
1468 ya_rand_init (0); // Now's a good time.
1472 "You must call this method before attempting to get orientation data from
1473 the receiver. This method enables the device's accelerometer hardware
1474 and begins the delivery of acceleration events to the receiver."
1476 Adding or removing this doesn't seem to make any difference. It's
1477 probably getting called by the UINavigationController. Still... */
1478 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
1480 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1483 if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
1484 rotating_nav.view.hidden = YES;
1486 [window setRootViewController: rotating_nav];
1487 [window setAutoresizesSubviews:YES];
1488 [window setAutoresizingMask:
1489 (UIViewAutoresizingFlexibleWidth |
1490 UIViewAutoresizingFlexibleHeight)];
1492 SaverListController *menu = [[SaverListController alloc]
1493 initWithNames:saverNames
1494 descriptions:[self makeDescTable]];
1495 [rotating_nav pushViewController:menu animated:YES];
1496 [menu becomeFirstResponder];
1499 application.applicationSupportsShakeToEdit = YES;
1502 # endif // USE_IPHONE
1504 NSString *forced = 0;
1505 /* In the XCode project, each .saver scheme sets this env var when
1506 launching SaverTester.app so that it knows which one we are
1507 currently debugging. If this is set, it overrides the default
1508 selection in the popup menu. If unset, that menu persists to
1509 whatever it was last time.
1511 const char *f = getenv ("SELECTED_SAVER");
1513 forced = [NSString stringWithCString:(char *)f
1514 encoding:NSUTF8StringEncoding];
1516 if (forced && ![saverNames containsObject:forced]) {
1517 NSLog(@"forced saver \"%@\" does not exist", forced);
1521 // If there's only one saver, run that.
1522 if (!forced && [saverNames count] == 1)
1523 forced = [saverNames objectAtIndex:0];
1526 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1531 // If nothing was selected (e.g., this is the first launch)
1532 // then scroll randomly instead of starting up at "A".
1535 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1538 [menu scrollTo: prev];
1539 # endif // USE_IPHONE
1542 [prefs setObject:forced forKey:@"selectedSaverName"];
1545 /* Don't auto-launch the saver unless it was running last time.
1546 XScreenSaverView manages this, on crash_timer.
1549 if (!forced && ![prefs boolForKey:@"wasRunning"])
1553 [self selectedSaverDidChange:nil];
1554 // [NSTimer scheduledTimerWithTimeInterval: 0
1556 // selector:@selector(selectedSaverDidChange:)
1563 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1564 ScreenSaverView to run its own timer calling animateOneFrame.
1565 On 10.9, that fails because the private class ScreenSaverModule
1566 is only initialized properly by ScreenSaverEngine, and in the
1567 context of SaverRunner, the null ScreenSaverEngine instance
1568 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1569 So, if it looks like this is the 10.9 version of ScreenSaverModule
1570 instead of the 10.8 version, we run our own timer here. This sucks.
1573 Class ssm = NSClassFromString (@"ScreenSaverModule");
1574 if (ssm && [ssm instancesRespondToSelector:
1575 @selector(needsAnimationTimer)]) {
1576 NSWindow *win = [windows objectAtIndex:0];
1577 ScreenSaverView *sv = find_saverView ([win contentView]);
1578 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1579 [sv animationTimeInterval]
1581 selector:@selector(animTimer)
1586 # endif // !USE_IPHONE
1592 /* When the window closes, exit (even if prefs still open.)
1594 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1599 # else // USE_IPHONE
1601 - (void)applicationWillResignActive:(UIApplication *)app
1603 [(XScreenSaverView *)view setScreenLocked:YES];
1606 - (void)applicationDidBecomeActive:(UIApplication *)app
1608 [(XScreenSaverView *)view setScreenLocked:NO];
1611 - (void)applicationDidEnterBackground:(UIApplication *)application
1613 [(XScreenSaverView *)view setScreenLocked:YES];
1616 #endif // USE_IPHONE