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 */
390 NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 ?
391 ![_saverView suppressRotationAnimation] :
396 - (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */
398 // Lies from the iOS docs:
399 // "This method is only called if the view controller's shouldAutorotate
400 // method returns YES."
401 return UIInterfaceOrientationMaskAll;
406 - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
408 return UIInterfaceOrientationPortrait;
413 - (void)setSaverName:(NSString *)name
416 [_saverName release];
418 // _storedOrientation =
419 // [UIApplication sharedApplication].statusBarOrientation;
422 [self createSaverView];
426 - (void)viewWillTransitionToSize: (CGSize)size
427 withTransitionCoordinator:
428 (id<UIViewControllerTransitionCoordinator>) coordinator
430 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
435 [CATransaction begin];
437 // Completely suppress the rotation animation, since we
438 // will not (visually) be rotating at all.
439 if ([_saverView suppressRotationAnimation])
440 [CATransaction setDisableActions:YES];
442 [self aboutOff:TRUE]; // It does goofy things if we rotate while it's up
444 [coordinator animateAlongsideTransition:^
445 (id <UIViewControllerTransitionCoordinatorContext> context) {
446 // This executes repeatedly during the rotation.
447 } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
448 // This executes once when the rotation has finished.
449 [CATransaction commit];
450 [_saverView orientationChanged];
452 // No code goes here, as it would execute before the above completes.
460 @implementation SaverRunner
463 - (XScreenSaverView *) newSaverView: (NSString *) module
464 withSize: (NSSize) size
470 // Load the XScreenSaverView subclass and code from a ".saver" bundle.
472 NSString *name = [module stringByAppendingPathExtension:@"saver"];
473 NSString *path = [saverDir stringByAppendingPathComponent:name];
475 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
476 NSLog(@"bundle \"%@\" does not exist", path);
480 NSLog(@"Loading %@", path);
482 // NSBundle *obundle = saverBundle;
484 saverBundle = [NSBundle bundleWithPath:path];
486 new_class = [saverBundle principalClass];
488 // Not entirely unsurprisingly, this tends to break the world.
489 // if (obundle && obundle != saverBundle)
494 // Determine whether to create an X11 view or an OpenGL view by
495 // looking for the "gl" tag in the xml file. This is kind of awful.
497 NSString *path = [saverDir
498 stringByAppendingPathComponent:
499 [[[module lowercaseString]
500 stringByReplacingOccurrencesOfString:@" "
502 stringByAppendingPathExtension:@"xml"]];
503 NSData *xmld = [NSData dataWithContentsOfFile:path];
504 NSAssert (xmld, @"no XML: %@", path);
505 NSString *xml = [XScreenSaverView decompressXML:xmld];
506 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
509 ? [XScreenSaverGLView class]
510 : [XScreenSaverView class]);
512 # endif // USE_IPHONE
518 rect.origin.x = rect.origin.y = 0;
519 rect.size.width = size.width;
520 rect.size.height = size.height;
522 XScreenSaverView *instance =
523 [(XScreenSaverView *) [new_class alloc]
528 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
533 /* KLUGE: Inform the underlying program that we're in "standalone"
534 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
535 This is kind of horrible but I haven't thought of a more sensible
536 way to make this work.
539 if ([saverNames count] == 1) {
540 setenv ("XSCREENSAVER_STANDALONE", "1", 1);
544 return (XScreenSaverView *) instance;
550 static ScreenSaverView *
551 find_saverView_child (NSView *v)
553 NSArray *kids = [v subviews];
554 int nkids = [kids count];
556 for (i = 0; i < nkids; i++) {
557 NSObject *kid = [kids objectAtIndex:i];
558 if ([kid isKindOfClass:[ScreenSaverView class]]) {
559 return (ScreenSaverView *) kid;
561 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
569 static ScreenSaverView *
570 find_saverView (NSView *v)
573 NSView *p = [v superview];
577 return find_saverView_child (v);
581 /* Changes the contents of the menubar menus to correspond to
582 the running saver. Desktop only.
585 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
587 if ([v isKindOfClass:[NSMenu class]]) {
588 NSMenu *m = (NSMenu *)v;
589 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
590 withString:new_str]];
591 NSArray *kids = [m itemArray];
592 int nkids = [kids count];
594 for (i = 0; i < nkids; i++) {
595 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
597 } else if ([v isKindOfClass:[NSMenuItem class]]) {
598 NSMenuItem *mi = (NSMenuItem *)v;
599 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
600 withString:new_str]];
601 NSMenu *m = [mi submenu];
602 if (m) relabel_menus (m, old_str, new_str);
607 - (void) openPreferences: (id) sender
610 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
611 sv = find_saverView ((NSView *) sender);
615 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
616 w = [windows objectAtIndex:i];
617 if ([w isKeyWindow]) break;
619 sv = find_saverView ([w contentView]);
622 NSAssert (sv, @"no saver view");
624 NSWindow *prefs = [sv configureSheet];
626 [NSApp beginSheet:prefs
627 modalForWindow:[sv window]
629 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
631 int code = [NSApp runModalForWindow:prefs];
633 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
634 We have to restart *both* animations, because the xlockmore-style
635 ones will blow up if one re-inits but the other doesn't.
637 if (code != NSCancelButton) {
638 if ([sv isAnimating])
645 - (void) preferencesClosed: (NSWindow *) sheet
646 returnCode: (int) returnCode
647 contextInfo: (void *) contextInfo
649 [NSApp stopModalWithCode:returnCode];
655 - (UIImage *) screenshot
657 return saved_screenshot;
660 - (void) saveScreenshot
662 // Most of this is from:
663 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
664 // The rotation stuff is by me.
666 CGSize size = [[UIScreen mainScreen] bounds].size;
668 // iOS 7: Needs to be the actual device orientation.
669 // iOS 8: Needs to be UIInterfaceOrientationPortrait.
671 UIInterfaceOrientation orient =
672 NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 ?
673 UIInterfaceOrientationPortrait /* iOS 8 broke -[UIScreen bounds]. */ :
674 [[window rootViewController] interfaceOrientation];
676 if (orient == UIInterfaceOrientationLandscapeLeft ||
677 orient == UIInterfaceOrientationLandscapeRight) {
678 // Rotate the shape of the canvas 90 degrees.
679 double s = size.width;
680 size.width = size.height;
685 // Create a graphics context with the target size
686 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
687 // take the scale into consideration
688 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
690 if (UIGraphicsBeginImageContextWithOptions)
691 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
693 UIGraphicsBeginImageContext (size);
695 CGContextRef ctx = UIGraphicsGetCurrentContext();
698 // Rotate the graphics context to match current hardware rotation.
701 case UIInterfaceOrientationPortraitUpsideDown:
702 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
703 CGContextRotateCTM (ctx, M_PI);
704 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
706 case UIInterfaceOrientationLandscapeLeft:
707 case UIInterfaceOrientationLandscapeRight:
708 CGContextTranslateCTM (ctx,
709 ([window frame].size.height -
710 [window frame].size.width) / 2,
711 ([window frame].size.width -
712 [window frame].size.height) / 2);
713 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
714 CGContextRotateCTM (ctx,
715 (orient == UIInterfaceOrientationLandscapeLeft
718 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
724 // Iterate over every window from back to front
726 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
727 if (![win respondsToSelector:@selector(screen)] ||
728 [win screen] == [UIScreen mainScreen]) {
730 // -renderInContext: renders in the coordinate space of the layer,
731 // so we must first apply the layer's geometry to the graphics context
732 CGContextSaveGState (ctx);
734 // Center the context around the window's anchor point
735 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
737 // Apply the window's transform about the anchor point
738 CGContextConcatCTM (ctx, [win transform]);
740 // Offset by the portion of the bounds left of and above anchor point
741 CGContextTranslateCTM (ctx,
742 -[win bounds].size.width * [[win layer] anchorPoint].x,
743 -[win bounds].size.height * [[win layer] anchorPoint].y);
745 // Render the layer hierarchy to the current context
746 [[win layer] renderInContext:ctx];
748 // Restore the context
749 CGContextRestoreGState (ctx);
753 if (saved_screenshot)
754 [saved_screenshot release];
755 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
757 UIGraphicsEndImageContext();
761 - (void) openPreferences: (NSString *) saver
763 XScreenSaverView *saverView = [self newSaverView:saver
764 withSize:CGSizeMake(0, 0)];
765 if (! saverView) return;
767 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
768 [prefs setObject:saver forKey:@"selectedSaverName"];
771 [rotating_nav pushViewController: [saverView configureView]
780 - (void)loadSaver:(NSString *)name
784 if (saverName && [saverName isEqualToString: name]) {
785 for (NSWindow *win in windows) {
786 ScreenSaverView *sv = find_saverView ([win contentView]);
787 if (![sv isAnimating])
795 for (NSWindow *win in windows) {
796 NSView *cv = [win contentView];
797 NSString *old_title = [win title];
798 if (!old_title) old_title = @"XScreenSaver";
799 [win setTitle: name];
800 relabel_menus (menubar, old_title, name);
802 ScreenSaverView *old_view = find_saverView (cv);
803 NSView *sup = old_view ? [old_view superview] : cv;
806 if ([old_view isAnimating])
807 [old_view stopAnimation];
808 [old_view removeFromSuperview];
811 NSSize size = [cv frame].size;
812 ScreenSaverView *new_view = [self newSaverView:name withSize: size];
813 NSAssert (new_view, @"unable to make a saver view");
815 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
816 [sup addSubview: new_view];
817 [win makeFirstResponder:new_view];
818 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
819 [new_view startAnimation];
823 NSUserDefaultsController *ctl =
824 [NSUserDefaultsController sharedUserDefaultsController];
829 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
830 NSLog (@"selecting saver \"%@\"", name);
833 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
834 [prefs setObject:name forKey:@"selectedSaverName"];
837 /* Cacheing this screws up rotation when starting a saver twice in a row.
838 if (saverName && [saverName isEqualToString: name]) {
839 if ([saverView isAnimating])
848 if (nonrotating_controller) {
849 nonrotating_controller.saverName = name;
853 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
854 UIScreen *screen = [UIScreen mainScreen];
856 /* 'nativeScale' is very confusing.
859 bounds: 320x480 scale: 2
860 nativeBounds: 640x960 nativeScale: 2
862 bounds: 320x568 scale: 2
863 nativeBounds: 640x1136 nativeScale: 2
865 bounds: 768x1024 scale: 1
866 nativeBounds: 768x1024 nativeScale: 1
868 bounds: 768x1024 scale: 2
869 nativeBounds: 1536x2048 nativeScale: 2
871 bounds: 320x568 scale: 2
872 nativeBounds: 640x1136 nativeScale: 2
874 bounds: 320x568 scale: 2
875 nativeBounds: 960x1704 nativeScale: 3
877 According to a StackOverflow comment:
879 The iPhone 6+ renders internally using @3x assets at a virtual
880 resolution of 2208x1242 (with 736x414 points), then samples that down
881 for display. The same as using a scaled resolution on a Retina MacBook
882 -- it lets them hit an integral multiple for pixel assets while still
883 having e.g. 12pt text look the same size on the screen.
885 The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
886 and use @2x assets to stick to the approximately 160 points per inch
887 of all previous devices.
889 The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
890 @2.46x assets. Instead Apple uses @3x assets and scales the complete
891 output down to about 84% of its natural size.
893 In practice Apple has decided to go with more like 87%, turning the
894 1080 into 1242. No doubt that was to find something as close as
895 possible to 84% that still produced integral sizes in both directions
896 -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
897 into, say, 1286, you'd somehow need to render 2286.22 pixels
898 vertically to scale well.
901 NSLog(@"screen: %.0fx%0.f",
902 [[screen currentMode] size].width,
903 [[screen currentMode] size].height);
904 NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
905 [screen bounds].size.width,
906 [screen bounds].size.height,
908 [screen scale] * [screen bounds].size.width,
909 [screen scale] * [screen bounds].size.height);
912 if ([screen respondsToSelector:@selector(nativeBounds)])
913 NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
914 [screen nativeBounds].size.width,
915 [screen nativeBounds].size.height,
916 [screen nativeScale],
917 [screen nativeBounds].size.width / [screen nativeScale],
918 [screen nativeBounds].size.height / [screen nativeScale]);
920 # endif // TARGET_IPHONE_SIMULATOR
922 // Take the screen shot before creating the screen saver view, because this
923 // can screw with the layout.
924 [self saveScreenshot];
926 // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal
927 // presentation full screen.
928 rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen;
930 nonrotating_controller = [[SaverViewController alloc]
931 initWithSaverRunner:self
932 showAboutBox:[saverNames count] != 1];
933 nonrotating_controller.saverName = name;
937 [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil];
939 // Doing this makes savers cut back to the list instead of fading,
940 // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
941 // [window setHidden:YES];
943 # endif // USE_IPHONE
949 - (void)aboutPanel:(id)sender
951 NSDictionary *bd = [saverBundle infoDictionary];
952 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
954 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
955 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
956 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
957 forKey:@"ApplicationVersion"];
958 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
959 NSAttributedString *s = [[NSAttributedString alloc]
960 initWithString: (NSString *)
961 [bd objectForKey:@"CFBundleGetInfoString"]];
962 [d setValue:s forKey:@"Credits"];
965 [[NSApplication sharedApplication]
966 orderFrontStandardAboutPanelWithOptions:d];
969 #endif // !USE_IPHONE
973 - (void)selectedSaverDidChange:(NSDictionary *)change
975 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
976 NSString *name = [prefs stringForKey:@"selectedSaverName"];
980 if (! [saverNames containsObject:name]) {
981 NSLog (@"saver \"%@\" does not exist", name);
985 [self loadSaver: name];
989 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
992 NSString *ext = @"saver";
994 NSString *ext = @"xml";
997 NSArray *files = [[NSFileManager defaultManager]
998 contentsOfDirectoryAtPath:dir error:nil];
999 if (! files) return 0;
1000 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
1002 for (NSString *p in files) {
1003 if ([[p pathExtension] caseInsensitiveCompare: ext])
1006 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
1009 // Get the saver name's capitalization right by reading the XML file.
1011 p = [dir stringByAppendingPathComponent: p];
1012 NSData *xmld = [NSData dataWithContentsOfFile:p];
1013 NSAssert (xmld, @"no XML: %@", p);
1014 NSString *xml = [XScreenSaverView decompressXML:xmld];
1015 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
1016 NSAssert1 (r.length, @"no name in %@", p);
1018 xml = [xml substringFromIndex: r.location + r.length];
1019 r = [xml rangeOfString:@"\"" options:0];
1020 if (r.length) name = [xml substringToIndex: r.location];
1023 # endif // USE_IPHONE
1025 NSAssert1 (name, @"no name in %@", p);
1026 if (name) [result addObject: name];
1029 if (! [result count])
1037 - (NSArray *) listSaverBundleNames
1039 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
1042 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
1043 // directories in the bundle.
1044 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
1045 stringByAppendingPathComponent:@"Contents"]
1046 stringByAppendingPathComponent:@"Resources"]];
1047 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
1049 // Also look in the same directory as the executable.
1050 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
1051 stringByDeletingLastPathComponent]];
1053 // Finally, look in standard MacOS screensaver directories.
1054 // [dirs addObject: @"~/Library/Screen Savers"];
1055 // [dirs addObject: @"/Library/Screen Savers"];
1056 // [dirs addObject: @"/System/Library/Screen Savers"];
1058 # else // USE_IPHONE
1060 // On iOS, only look in the bundle's root directory.
1061 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
1063 # endif // USE_IPHONE
1066 for (i = 0; i < [dirs count]; i++) {
1067 NSString *dir = [dirs objectAtIndex:i];
1068 NSArray *names = [self listSaverBundleNamesInDir:dir];
1069 if (! names) continue;
1070 saverDir = [dir retain];
1071 saverNames = [names retain];
1075 NSString *err = @"no .saver bundles found in: ";
1076 for (i = 0; i < [dirs count]; i++) {
1077 if (i) err = [err stringByAppendingString:@", "];
1078 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
1079 stringByAbbreviatingWithTildeInPath]];
1080 err = [err stringByAppendingString:@"/"];
1083 return [NSArray array];
1087 /* Create the popup menu of available saver names.
1091 - (NSPopUpButton *) makeMenu
1094 rect.origin.x = rect.origin.y = 0;
1095 rect.size.width = 10;
1096 rect.size.height = 10;
1097 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1100 float max_width = 0;
1101 for (i = 0; i < [saverNames count]; i++) {
1102 NSString *name = [saverNames objectAtIndex:i];
1103 [popup addItemWithTitle:name];
1104 [[popup itemWithTitle:name] setRepresentedObject:name];
1106 NSRect r = [popup frame];
1107 if (r.size.width > max_width) max_width = r.size.width;
1110 // Bind the menu to preferences, and trigger a callback when an item
1113 NSString *key = @"values.selectedSaverName";
1114 NSUserDefaultsController *prefs =
1115 [NSUserDefaultsController sharedUserDefaultsController];
1116 [prefs addObserver:self
1119 context:@selector(selectedSaverDidChange:)];
1120 [popup bind:@"selectedObject"
1124 [prefs setAppliesImmediately:YES];
1126 NSRect r = [popup frame];
1127 r.size.width = max_width;
1129 [popup autorelease];
1135 - (NSString *) makeDesc:(NSString *)saver
1136 yearOnly:(BOOL) yearp
1139 NSString *path = [saverDir stringByAppendingPathComponent:
1140 [[saver lowercaseString]
1141 stringByReplacingOccurrencesOfString:@" "
1145 path = [path stringByAppendingPathExtension:@"xml"];
1146 NSData *xmld = [NSData dataWithContentsOfFile:path];
1147 if (! xmld) goto FAIL;
1148 desc = [XScreenSaverView decompressXML:xmld];
1149 if (! desc) goto FAIL;
1151 r = [desc rangeOfString:@"<_description>"
1152 options:NSCaseInsensitiveSearch];
1153 if (r.length == 0) {
1157 desc = [desc substringFromIndex: r.location + r.length];
1158 r = [desc rangeOfString:@"</_description>"
1159 options:NSCaseInsensitiveSearch];
1161 desc = [desc substringToIndex: r.location];
1163 // Leading and trailing whitespace.
1164 desc = [desc stringByTrimmingCharactersInSet:
1165 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1167 // Let's see if we can find a year on the last line.
1168 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
1170 for (NSString *word in
1171 [[desc substringFromIndex:r.location + r.length]
1172 componentsSeparatedByCharactersInSet:
1173 [NSCharacterSet characterSetWithCharactersInString:
1175 int n = [word doubleValue];
1176 if (n > 1970 && n < 2100)
1180 // Delete everything after the first blank line.
1182 r = [desc rangeOfString:@"\n\n" options:0];
1184 desc = [desc substringToIndex: r.location];
1186 // Unwrap lines and compress whitespace.
1188 NSString *result = @"";
1189 for (NSString *s in [desc componentsSeparatedByCharactersInSet:
1190 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
1191 if ([result length] == 0)
1193 else if ([s length] > 0)
1194 result = [NSString stringWithFormat: @"%@ %@", result, s];
1200 desc = [year stringByAppendingString:
1201 [@": " stringByAppendingString: desc]];
1204 desc = year ? year : @"";
1208 if ([saverNames count] > 1)
1209 desc = @"Oops, this module appears to be incomplete.";
1217 - (NSString *) makeDesc:(NSString *)saver
1219 return [self makeDesc:saver yearOnly:NO];
1224 /* Create a dictionary of one-line descriptions of every saver,
1225 for display on the UITableView.
1227 - (NSDictionary *)makeDescTable
1229 NSMutableDictionary *dict =
1230 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1231 for (NSString *saver in saverNames) {
1232 [dict setObject:[self makeDesc:saver] forKey:saver];
1238 - (void) wantsFadeOut:(XScreenSaverView *)sender
1240 rotating_nav.view.hidden = NO; // In case it was hidden during startup.
1242 /* The XScreenSaverView screws with the status bar orientation, mostly to
1243 keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0
1244 and/or 8.2), this confuses the UINavigationController, so put the
1245 orientation back to portrait before dismissing the SaverViewController.
1248 [[UIApplication sharedApplication]
1249 setStatusBarOrientation:UIInterfaceOrientationPortrait
1253 /* Make sure the most-recently-run saver is visible. Sometimes it ends
1254 up scrolled half a line off the bottom of the screen.
1257 for (UIViewController *v in [rotating_nav viewControllers]) {
1258 if ([v isKindOfClass:[SaverListController class]]) {
1259 [(SaverListController *)v scrollTo: saverName];
1265 [rotating_nav dismissViewControllerAnimated:YES completion:^() {
1266 [nonrotating_controller release];
1267 nonrotating_controller = nil;
1268 [[rotating_nav view] becomeFirstResponder];
1273 - (void) didShake:(XScreenSaverView *)sender
1275 # if TARGET_IPHONE_SIMULATOR
1276 NSLog (@"simulating shake on saver list");
1278 [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
1283 #endif // USE_IPHONE
1287 /* This is called when the "selectedSaverName" pref changes, e.g.,
1288 when a menu selection is made.
1290 - (void)observeValueForKeyPath:(NSString *)keyPath
1292 change:(NSDictionary *)change
1293 context:(void *)context
1295 SEL dispatchSelector = (SEL)context;
1296 if (dispatchSelector != NULL) {
1297 [self performSelector:dispatchSelector withObject:change];
1299 [super observeValueForKeyPath:keyPath
1309 /* Create the desktop window shell, possibly including a preferences button.
1311 - (NSWindow *) makeWindow
1314 static int count = 0;
1315 Bool simple_p = ([saverNames count] == 1);
1317 NSPopUpButton *menu = 0;
1322 sv_rect.origin.x = sv_rect.origin.y = 0;
1323 sv_rect.size.width = 320;
1324 sv_rect.size.height = 240;
1325 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
1326 initWithFrame:sv_rect
1329 // make a "Preferences" button
1334 rect.size.width = rect.size.height = 10;
1335 pb = [[NSButton alloc] initWithFrame:rect];
1336 [pb setTitle:@"Preferences"];
1337 [pb setBezelStyle:NSRoundedBezelStyle];
1340 rect.origin.x = ([sv frame].size.width -
1341 [pb frame].size.width) / 2;
1342 [pb setFrameOrigin:rect.origin];
1346 [pb setTarget:self];
1347 [pb setAction:@selector(openPreferences:)];
1349 // Make a saver selection menu
1351 menu = [self makeMenu];
1354 [menu setFrameOrigin:rect.origin];
1356 // make a box to wrap the saverView
1360 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1361 gbox = [[NSBox alloc] initWithFrame:rect];
1362 rect.size.width = rect.size.height = 10;
1363 [gbox setContentViewMargins:rect.size];
1364 [gbox setTitlePosition:NSNoTitle];
1365 [gbox addSubview:sv];
1368 // make a box to wrap the other two boxes
1370 rect.origin.x = rect.origin.y = 0;
1371 rect.size.width = [gbox frame].size.width;
1372 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1373 pbox = [[NSBox alloc] initWithFrame:rect];
1374 [pbox setTitlePosition:NSNoTitle];
1375 [pbox setBorderType:NSNoBorder];
1376 [pbox addSubview:gbox];
1378 if (menu) [pbox addSubview:menu];
1379 if (pb) [pbox addSubview:pb];
1383 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1384 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1385 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1386 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1389 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1392 // and make a window to hold that.
1394 NSScreen *screen = [NSScreen mainScreen];
1395 rect = pbox ? [pbox frame] : [sv frame];
1396 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1397 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1399 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1401 NSWindow *win = [[NSWindow alloc]
1402 initWithContentRect:rect
1403 styleMask:(NSTitledWindowMask |
1404 NSClosableWindowMask |
1405 NSMiniaturizableWindowMask |
1406 NSResizableWindowMask)
1407 backing:NSBackingStoreBuffered
1410 [win setMinSize:[win frameRectForContentRect:rect].size];
1411 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1414 [win makeKeyAndOrderFront:win];
1416 [sv startAnimation]; // this is the dummy saver
1427 for (NSWindow *win in windows) {
1428 ScreenSaverView *sv = find_saverView ([win contentView]);
1429 if ([sv isAnimating])
1430 [sv animateOneFrame];
1434 # endif // !USE_IPHONE
1437 - (void)applicationDidFinishLaunching:
1439 (NSNotification *) notif
1440 # else // USE_IPHONE
1441 (UIApplication *) application
1442 # endif // USE_IPHONE
1444 [self listSaverBundleNames];
1446 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1449 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1450 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1455 // Create either one window (for standalone, e.g. Phosphor.app)
1456 // or two windows for SaverTester.app.
1457 for (i = 0; i < window_count; i++) {
1458 NSWindow *win = [self makeWindow];
1459 [win setDelegate:self];
1460 // Get the last-saved window position out of preferences.
1461 [win setFrameAutosaveName:
1462 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1463 [win setFrameUsingName:[win frameAutosaveName]];
1465 // This prevents clicks from being seen by savers.
1466 // [win setMovableByWindowBackground:YES];
1467 win.releasedWhenClosed = NO;
1470 # else // USE_IPHONE
1472 # undef ya_rand_init
1473 ya_rand_init (0); // Now's a good time.
1477 "You must call this method before attempting to get orientation data from
1478 the receiver. This method enables the device's accelerometer hardware
1479 and begins the delivery of acceleration events to the receiver."
1481 Adding or removing this doesn't seem to make any difference. It's
1482 probably getting called by the UINavigationController. Still... */
1483 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
1485 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1488 if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
1489 rotating_nav.view.hidden = YES;
1491 [window setRootViewController: rotating_nav];
1492 [window setAutoresizesSubviews:YES];
1493 [window setAutoresizingMask:
1494 (UIViewAutoresizingFlexibleWidth |
1495 UIViewAutoresizingFlexibleHeight)];
1497 SaverListController *menu = [[SaverListController alloc]
1498 initWithNames:saverNames
1499 descriptions:[self makeDescTable]];
1500 [rotating_nav pushViewController:menu animated:YES];
1501 [menu becomeFirstResponder];
1504 application.applicationSupportsShakeToEdit = YES;
1507 # endif // USE_IPHONE
1509 NSString *forced = 0;
1510 /* In the XCode project, each .saver scheme sets this env var when
1511 launching SaverTester.app so that it knows which one we are
1512 currently debugging. If this is set, it overrides the default
1513 selection in the popup menu. If unset, that menu persists to
1514 whatever it was last time.
1516 const char *f = getenv ("SELECTED_SAVER");
1518 forced = [NSString stringWithCString:(char *)f
1519 encoding:NSUTF8StringEncoding];
1521 if (forced && ![saverNames containsObject:forced]) {
1522 NSLog(@"forced saver \"%@\" does not exist", forced);
1526 // If there's only one saver, run that.
1527 if (!forced && [saverNames count] == 1)
1528 forced = [saverNames objectAtIndex:0];
1531 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1536 // If nothing was selected (e.g., this is the first launch)
1537 // then scroll randomly instead of starting up at "A".
1540 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1543 [menu scrollTo: prev];
1544 # endif // USE_IPHONE
1547 [prefs setObject:forced forKey:@"selectedSaverName"];
1550 /* Don't auto-launch the saver unless it was running last time.
1551 XScreenSaverView manages this, on crash_timer.
1554 if (!forced && ![prefs boolForKey:@"wasRunning"])
1558 [self selectedSaverDidChange:nil];
1559 // [NSTimer scheduledTimerWithTimeInterval: 0
1561 // selector:@selector(selectedSaverDidChange:)
1568 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1569 ScreenSaverView to run its own timer calling animateOneFrame.
1570 On 10.9, that fails because the private class ScreenSaverModule
1571 is only initialized properly by ScreenSaverEngine, and in the
1572 context of SaverRunner, the null ScreenSaverEngine instance
1573 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1574 So, if it looks like this is the 10.9 version of ScreenSaverModule
1575 instead of the 10.8 version, we run our own timer here. This sucks.
1578 Class ssm = NSClassFromString (@"ScreenSaverModule");
1579 if (ssm && [ssm instancesRespondToSelector:
1580 @selector(needsAnimationTimer)]) {
1581 NSWindow *win = [windows objectAtIndex:0];
1582 ScreenSaverView *sv = find_saverView ([win contentView]);
1583 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1584 [sv animationTimeInterval]
1586 selector:@selector(animTimer)
1591 # endif // !USE_IPHONE
1597 /* When the window closes, exit (even if prefs still open.)
1599 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1604 /* When the window is about to close, stop its animation.
1605 Without this, timers might fire after the window is dead.
1607 - (void)windowWillClose:(NSNotification *)notification
1609 NSWindow *win = [notification object];
1610 NSView *cv = win ? [win contentView] : 0;
1611 ScreenSaverView *sv = cv ? find_saverView (cv) : 0;
1612 if (sv && [sv isAnimating])
1616 # else // USE_IPHONE
1618 - (void)applicationWillResignActive:(UIApplication *)app
1620 [(XScreenSaverView *)view setScreenLocked:YES];
1623 - (void)applicationDidBecomeActive:(UIApplication *)app
1625 [(XScreenSaverView *)view setScreenLocked:NO];
1628 - (void)applicationDidEnterBackground:(UIApplication *)application
1630 [(XScreenSaverView *)view setScreenLocked:YES];
1633 #endif // USE_IPHONE