1 /* xscreensaver, Copyright (c) 2006-2017 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;
103 # ifndef __IPHONE_7_0
104 self.wantsFullScreenLayout = YES; // Deprecated as of iOS 7
110 - (BOOL) prefersStatusBarHidden
112 // Requires UIViewControllerBasedStatusBarAppearance = true in plist
118 [_saverName release];
119 // iOS: When a UIView deallocs, it doesn't do [UIView removeFromSuperView]
120 // for its subviews, so the subviews end up with a dangling pointer in their
121 // superview properties.
122 [aboutBox removeFromSuperview];
124 [_saverView removeFromSuperview];
125 [_saverView release];
132 // The UIViewController's view must never change, so it gets set here to
133 // a plain black background.
135 // This background view doesn't block the status bar, but that's probably
136 // OK, because it's never on screen for more than a fraction of a second.
137 UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull];
138 backgroundView.backgroundColor = [UIColor blackColor];
139 self.view = backgroundView;
140 [backgroundView release];
144 - (void)aboutPanel:(UIView *)saverView
145 orientation:(UIInterfaceOrientation)orient
150 NSString *name = _saverName;
151 NSString *year = [_parent makeDesc:_saverName yearOnly:YES];
154 CGRect frame = [saverView frame];
158 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
159 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
162 CGSize s = CGSizeMake(frame.size.width, frame.size.height);
163 CGSize tsize1 = [[[NSAttributedString alloc]
165 attributes:@{ NSFontAttributeName: font1 }]
166 boundingRectWithSize: s
167 options: NSStringDrawingUsesLineFragmentOrigin
169 CGSize tsize2 = [[[NSAttributedString alloc]
171 attributes:@{ NSFontAttributeName: font2 }]
172 boundingRectWithSize: s
173 options: NSStringDrawingUsesLineFragmentOrigin
175 # else // iOS 6 or Cocoa
176 CGSize tsize1 = [name sizeWithFont:font1
177 constrainedToSize:CGSizeMake(frame.size.width,
179 CGSize tsize2 = [year sizeWithFont:font2
180 constrainedToSize:CGSizeMake(frame.size.width,
184 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
185 tsize1.width : tsize2.width,
186 tsize1.height + tsize2.height);
188 tsize.width = ceilf(tsize.width);
189 tsize.height = ceilf(tsize.height);
191 // Don't know how to find inner margin of UITextView.
193 tsize.width += margin * 4;
194 tsize.height += margin * 2;
196 if ([saverView frame].size.width >= 768)
197 tsize.height += pt1 * 3; // extra bottom margin on iPad
199 frame = CGRectMake (0, 0, tsize.width, tsize.height);
201 /* Get the text oriented properly, and move it to the bottom of the
202 screen, since many savers have action in the middle.
205 case UIInterfaceOrientationLandscapeLeft:
207 frame.origin.x = ([saverView frame].size.width
208 - (tsize.width - tsize.height) / 2
210 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
212 case UIInterfaceOrientationLandscapeRight:
214 frame.origin.x = -(tsize.width - tsize.height) / 2;
215 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
217 case UIInterfaceOrientationPortraitUpsideDown:
219 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
224 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
225 frame.origin.y = [saverView frame].size.height - tsize.height;
230 [aboutBox removeFromSuperview];
234 aboutBox = [[UIView alloc] initWithFrame:frame];
236 aboutBox.transform = CGAffineTransformMakeRotation (rot);
237 aboutBox.backgroundColor = [UIColor clearColor];
239 /* There seems to be no easy way to stroke the font, so instead draw
240 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
241 a black shadow to each. (You'd think the shadow alone would be
242 enough, but there's no way to make it dark enough to be legible.)
244 for (int i = 0; i < 5; i++) {
245 UITextView *textview;
247 frame.origin.x = frame.origin.y = 0;
249 case 0: frame.origin.x = -off; break;
250 case 1: frame.origin.x = off; break;
251 case 2: frame.origin.y = -off; break;
252 case 3: frame.origin.y = off; break;
255 for (int j = 0; j < 2; j++) {
257 frame.origin.y = (j == 0 ? 0 : pt1);
258 textview = [[UITextView alloc] initWithFrame:frame];
259 textview.font = (j == 0 ? font1 : font2);
260 textview.text = (j == 0 ? name : year);
261 textview.textAlignment = NSTextAlignmentCenter;
262 textview.showsHorizontalScrollIndicator = NO;
263 textview.showsVerticalScrollIndicator = NO;
264 textview.scrollEnabled = NO;
265 textview.editable = NO;
266 textview.userInteractionEnabled = NO;
267 textview.backgroundColor = [UIColor clearColor];
268 textview.textColor = (i == 4
269 ? [UIColor yellowColor]
270 : [UIColor blackColor]);
272 CALayer *textLayer = (CALayer *)
273 [textview.layer.sublayers objectAtIndex:0];
274 textLayer.shadowColor = [UIColor blackColor].CGColor;
275 textLayer.shadowOffset = CGSizeMake(0, 0);
276 textLayer.shadowOpacity = 1;
277 textLayer.shadowRadius = 2;
279 [aboutBox addSubview:textview];
283 CABasicAnimation *anim =
284 [CABasicAnimation animationWithKeyPath:@"opacity"];
286 anim.repeatCount = 1;
287 anim.autoreverses = NO;
288 anim.fromValue = [NSNumber numberWithFloat:0.0];
289 anim.toValue = [NSNumber numberWithFloat:1.0];
290 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
292 [saverView addSubview:aboutBox];
295 [splashTimer invalidate];
298 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
300 selector:@selector(aboutOff)
308 [self aboutOff:FALSE];
311 - (void)aboutOff:(BOOL)fast
315 [splashTimer invalidate];
319 aboutBox.layer.opacity = 0;
323 CABasicAnimation *anim =
324 [CABasicAnimation animationWithKeyPath:@"opacity"];
326 anim.repeatCount = 1;
327 anim.autoreverses = NO;
328 anim.fromValue = [NSNumber numberWithFloat: 1];
329 anim.toValue = [NSNumber numberWithFloat: 0];
330 // anim.delegate = self;
331 aboutBox.layer.opacity = 0;
332 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
337 - (void)createSaverView
339 UIView *parentView = self.view;
342 [_saverView removeFromSuperview];
343 [_saverView release];
347 if (_storedOrientation != UIInterfaceOrientationUnknown) {
348 [[UIApplication sharedApplication]
349 setStatusBarOrientation:_storedOrientation
354 _saverView = [_parent newSaverView:_saverName
355 withSize:parentView.bounds.size];
358 UIAlertController *c = [UIAlertController
359 alertControllerWithTitle:@"Unable to load!"
361 preferredStyle:UIAlertControllerStyleAlert];
362 [c addAction: [UIAlertAction actionWithTitle: @"Bummer"
363 style: UIAlertActionStyleDefault
364 handler: ^(UIAlertAction *a) {
365 // #### Should expose the SaverListController...
367 [self presentViewController:c animated:YES completion:nil];
372 _saverView.delegate = _parent;
373 _saverView.autoresizingMask =
374 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
376 [self.view addSubview:_saverView];
378 // The first responder must be set only after the view was placed in the view
380 [_saverView becomeFirstResponder]; // For shakes on iOS 6.
381 [_saverView startAnimation];
382 [self aboutPanel:_saverView
383 orientation:/* _storedOrientation */ UIInterfaceOrientationPortrait];
387 - (void)viewDidAppear:(BOOL)animated
389 [super viewDidAppear:animated];
390 [self createSaverView];
394 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
396 return NO; /* Deprecated in iOS 6 */
400 - (BOOL)shouldAutorotate /* Added in iOS 6 */
403 NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 ?
404 ![_saverView suppressRotationAnimation] :
409 - (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */
411 // Lies from the iOS docs:
412 // "This method is only called if the view controller's shouldAutorotate
413 // method returns YES."
414 return UIInterfaceOrientationMaskAll;
419 - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
421 return UIInterfaceOrientationPortrait;
426 - (void)setSaverName:(NSString *)name
429 [_saverName release];
431 // _storedOrientation =
432 // [UIApplication sharedApplication].statusBarOrientation;
435 [self createSaverView];
439 - (void)viewWillTransitionToSize: (CGSize)size
440 withTransitionCoordinator:
441 (id<UIViewControllerTransitionCoordinator>) coordinator
443 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
448 [CATransaction begin];
450 // Completely suppress the rotation animation, since we
451 // will not (visually) be rotating at all.
452 if ([_saverView suppressRotationAnimation])
453 [CATransaction setDisableActions:YES];
455 [self aboutOff:TRUE]; // It does goofy things if we rotate while it's up
457 [coordinator animateAlongsideTransition:^
458 (id <UIViewControllerTransitionCoordinatorContext> context) {
459 // This executes repeatedly during the rotation.
460 } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
461 // This executes once when the rotation has finished.
462 [CATransaction commit];
463 [_saverView orientationChanged];
465 // No code goes here, as it would execute before the above completes.
473 @implementation SaverRunner
476 - (XScreenSaverView *) newSaverView: (NSString *) module
477 withSize: (NSSize) size
483 // Load the XScreenSaverView subclass and code from a ".saver" bundle.
485 NSString *name = [module stringByAppendingPathExtension:@"saver"];
486 NSString *path = [saverDir stringByAppendingPathComponent:name];
488 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
489 NSLog(@"bundle \"%@\" does not exist", path);
493 NSLog(@"Loading %@", path);
495 // NSBundle *obundle = saverBundle;
497 saverBundle = [NSBundle bundleWithPath:path];
499 new_class = [saverBundle principalClass];
501 // Not entirely unsurprisingly, this tends to break the world.
502 // if (obundle && obundle != saverBundle)
507 // Determine whether to create an X11 view or an OpenGL view by
508 // looking for the "gl" tag in the xml file. This is kind of awful.
510 NSString *path = [saverDir
511 stringByAppendingPathComponent:
512 [[[module lowercaseString]
513 stringByReplacingOccurrencesOfString:@" "
515 stringByAppendingPathExtension:@"xml"]];
516 NSData *xmld = [NSData dataWithContentsOfFile:path];
517 NSAssert (xmld, @"no XML: %@", path);
518 NSString *xml = [XScreenSaverView decompressXML:xmld];
519 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
522 ? [XScreenSaverGLView class]
523 : [XScreenSaverView class]);
525 # endif // USE_IPHONE
531 rect.origin.x = rect.origin.y = 0;
532 rect.size.width = size.width;
533 rect.size.height = size.height;
535 XScreenSaverView *instance =
536 [(XScreenSaverView *) [new_class alloc]
541 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
546 /* KLUGE: Inform the underlying program that we're in "standalone"
547 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
548 This is kind of horrible but I haven't thought of a more sensible
549 way to make this work.
552 if ([saverNames count] == 1) {
553 setenv ("XSCREENSAVER_STANDALONE", "1", 1);
557 return (XScreenSaverView *) instance;
563 static ScreenSaverView *
564 find_saverView_child (NSView *v)
566 NSArray *kids = [v subviews];
567 NSUInteger nkids = [kids count];
569 for (i = 0; i < nkids; i++) {
570 NSObject *kid = [kids objectAtIndex:i];
571 if ([kid isKindOfClass:[ScreenSaverView class]]) {
572 return (ScreenSaverView *) kid;
574 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
582 static ScreenSaverView *
583 find_saverView (NSView *v)
586 NSView *p = [v superview];
590 return find_saverView_child (v);
594 /* Changes the contents of the menubar menus to correspond to
595 the running saver. Desktop only.
598 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
600 if ([v isKindOfClass:[NSMenu class]]) {
601 NSMenu *m = (NSMenu *)v;
602 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
603 withString:new_str]];
604 NSArray *kids = [m itemArray];
605 NSUInteger nkids = [kids count];
607 for (i = 0; i < nkids; i++) {
608 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
610 } else if ([v isKindOfClass:[NSMenuItem class]]) {
611 NSMenuItem *mi = (NSMenuItem *)v;
612 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
613 withString:new_str]];
614 NSMenu *m = [mi submenu];
615 if (m) relabel_menus (m, old_str, new_str);
620 - (void) openPreferences: (id) sender
623 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
624 sv = find_saverView ((NSView *) sender);
628 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
629 w = [windows objectAtIndex:i];
630 if ([w isKeyWindow]) break;
632 sv = find_saverView ([w contentView]);
635 NSAssert (sv, @"no saver view");
637 NSWindow *prefs = [sv configureSheet];
639 [NSApp beginSheet:prefs
640 modalForWindow:[sv window]
642 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
644 NSUInteger code = [NSApp runModalForWindow:prefs];
646 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
647 We have to restart *both* animations, because the xlockmore-style
648 ones will blow up if one re-inits but the other doesn't.
650 if (code != NSCancelButton) {
651 if ([sv isAnimating])
658 - (void) preferencesClosed: (NSWindow *) sheet
659 returnCode: (int) returnCode
660 contextInfo: (void *) contextInfo
662 [NSApp stopModalWithCode:returnCode];
668 - (UIImage *) screenshot
670 return saved_screenshot;
673 - (void) saveScreenshot
675 // Most of this is from:
676 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
677 // The rotation stuff is by me.
679 CGSize size = [[UIScreen mainScreen] bounds].size;
681 // iOS 7: Needs to be [[window rootViewController] interfaceOrientation].
682 // iOS 8: Needs to be UIInterfaceOrientationPortrait.
683 // (interfaceOrientation deprecated in iOS 8)
685 UIInterfaceOrientation orient = UIInterfaceOrientationPortrait;
686 /* iOS 8 broke -[UIScreen bounds]. */
688 if (orient == UIInterfaceOrientationLandscapeLeft ||
689 orient == UIInterfaceOrientationLandscapeRight) {
690 // Rotate the shape of the canvas 90 degrees.
691 double s = size.width;
692 size.width = size.height;
697 // Create a graphics context with the target size
698 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
699 // take the scale into consideration
700 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
702 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
704 CGContextRef ctx = UIGraphicsGetCurrentContext();
707 // Rotate the graphics context to match current hardware rotation.
710 case UIInterfaceOrientationPortraitUpsideDown:
711 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
712 CGContextRotateCTM (ctx, M_PI);
713 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
715 case UIInterfaceOrientationLandscapeLeft:
716 case UIInterfaceOrientationLandscapeRight:
717 CGContextTranslateCTM (ctx,
718 ([window frame].size.height -
719 [window frame].size.width) / 2,
720 ([window frame].size.width -
721 [window frame].size.height) / 2);
722 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
723 CGContextRotateCTM (ctx,
724 (orient == UIInterfaceOrientationLandscapeLeft
727 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
733 // Iterate over every window from back to front
735 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
736 if (![win respondsToSelector:@selector(screen)] ||
737 [win screen] == [UIScreen mainScreen]) {
739 // -renderInContext: renders in the coordinate space of the layer,
740 // so we must first apply the layer's geometry to the graphics context
741 CGContextSaveGState (ctx);
743 // Center the context around the window's anchor point
744 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
746 // Apply the window's transform about the anchor point
747 CGContextConcatCTM (ctx, [win transform]);
749 // Offset by the portion of the bounds left of and above anchor point
750 CGContextTranslateCTM (ctx,
751 -[win bounds].size.width * [[win layer] anchorPoint].x,
752 -[win bounds].size.height * [[win layer] anchorPoint].y);
754 // Render the layer hierarchy to the current context
755 [[win layer] renderInContext:ctx];
757 // Restore the context
758 CGContextRestoreGState (ctx);
762 if (saved_screenshot)
763 [saved_screenshot release];
764 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
766 UIGraphicsEndImageContext();
770 - (void) openPreferences: (NSString *) saver
772 XScreenSaverView *saverView = [self newSaverView:saver
773 withSize:CGSizeMake(0, 0)];
774 if (! saverView) return;
776 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
777 [prefs setObject:saver forKey:@"selectedSaverName"];
780 [rotating_nav pushViewController: [saverView configureView]
789 - (void)loadSaver:(NSString *)name
793 if (saverName && [saverName isEqualToString: name]) {
794 for (NSWindow *win in windows) {
795 ScreenSaverView *sv = find_saverView ([win contentView]);
796 if (![sv isAnimating])
804 for (NSWindow *win in windows) {
805 NSView *cv = [win contentView];
806 NSString *old_title = [win title];
807 if (!old_title) old_title = @"XScreenSaver";
808 [win setTitle: name];
809 relabel_menus (menubar, old_title, name);
811 ScreenSaverView *old_view = find_saverView (cv);
812 NSView *sup = old_view ? [old_view superview] : cv;
815 if ([old_view isAnimating])
816 [old_view stopAnimation];
817 [old_view removeFromSuperview];
820 NSSize size = [cv frame].size;
821 ScreenSaverView *new_view = [self newSaverView:name withSize: size];
822 NSAssert (new_view, @"unable to make a saver view");
824 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
825 [sup addSubview: new_view];
826 [win makeFirstResponder:new_view];
827 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
828 [new_view startAnimation];
832 NSUserDefaultsController *ctl =
833 [NSUserDefaultsController sharedUserDefaultsController];
838 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
839 NSLog (@"selecting saver \"%@\"", name);
842 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
843 [prefs setObject:name forKey:@"selectedSaverName"];
846 /* Cacheing this screws up rotation when starting a saver twice in a row.
847 if (saverName && [saverName isEqualToString: name]) {
848 if ([saverView isAnimating])
857 if (nonrotating_controller) {
858 nonrotating_controller.saverName = name;
862 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
863 UIScreen *screen = [UIScreen mainScreen];
865 /* 'nativeScale' is very confusing.
868 bounds: 320x480 scale: 2
869 nativeBounds: 640x960 nativeScale: 2
871 bounds: 320x568 scale: 2
872 nativeBounds: 640x1136 nativeScale: 2
874 bounds: 768x1024 scale: 1
875 nativeBounds: 768x1024 nativeScale: 1
877 bounds: 768x1024 scale: 2
878 nativeBounds: 1536x2048 nativeScale: 2
880 bounds: 320x568 scale: 2
881 nativeBounds: 640x1136 nativeScale: 2
883 bounds: 320x568 scale: 2
884 nativeBounds: 960x1704 nativeScale: 3
886 According to a StackOverflow comment:
888 The iPhone 6+ renders internally using @3x assets at a virtual
889 resolution of 2208x1242 (with 736x414 points), then samples that down
890 for display. The same as using a scaled resolution on a Retina MacBook
891 -- it lets them hit an integral multiple for pixel assets while still
892 having e.g. 12pt text look the same size on the screen.
894 The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
895 and use @2x assets to stick to the approximately 160 points per inch
896 of all previous devices.
898 The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
899 @2.46x assets. Instead Apple uses @3x assets and scales the complete
900 output down to about 84% of its natural size.
902 In practice Apple has decided to go with more like 87%, turning the
903 1080 into 1242. No doubt that was to find something as close as
904 possible to 84% that still produced integral sizes in both directions
905 -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
906 into, say, 1286, you'd somehow need to render 2286.22 pixels
907 vertically to scale well.
910 NSLog(@"screen: %.0fx%0.f",
911 [[screen currentMode] size].width,
912 [[screen currentMode] size].height);
913 NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
914 [screen bounds].size.width,
915 [screen bounds].size.height,
917 [screen scale] * [screen bounds].size.width,
918 [screen scale] * [screen bounds].size.height);
921 if ([screen respondsToSelector:@selector(nativeBounds)])
922 NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
923 [screen nativeBounds].size.width,
924 [screen nativeBounds].size.height,
925 [screen nativeScale],
926 [screen nativeBounds].size.width / [screen nativeScale],
927 [screen nativeBounds].size.height / [screen nativeScale]);
929 # endif // TARGET_IPHONE_SIMULATOR
931 // Take the screen shot before creating the screen saver view, because this
932 // can screw with the layout.
933 [self saveScreenshot];
935 // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal
936 // presentation full screen.
937 rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen;
939 nonrotating_controller = [[SaverViewController alloc]
940 initWithSaverRunner:self
941 showAboutBox:[saverNames count] != 1];
942 nonrotating_controller.saverName = name;
946 [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil];
948 // Doing this makes savers cut back to the list instead of fading,
949 // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
950 // [window setHidden:YES];
952 # endif // USE_IPHONE
958 - (void)aboutPanel:(id)sender
960 NSDictionary *bd = [saverBundle infoDictionary];
961 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
963 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
964 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
965 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
966 forKey:@"ApplicationVersion"];
967 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
968 NSAttributedString *s = [[NSAttributedString alloc]
969 initWithString: (NSString *)
970 [bd objectForKey:@"CFBundleGetInfoString"]];
971 [d setValue:s forKey:@"Credits"];
974 [[NSApplication sharedApplication]
975 orderFrontStandardAboutPanelWithOptions:d];
978 #endif // !USE_IPHONE
982 - (void)selectedSaverDidChange:(NSDictionary *)change
984 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
985 NSString *name = [prefs stringForKey:@"selectedSaverName"];
989 if (! [saverNames containsObject:name]) {
990 NSLog (@"saver \"%@\" does not exist", name);
994 [self loadSaver: name];
998 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
1001 NSString *ext = @"saver";
1003 NSString *ext = @"xml";
1006 NSArray *files = [[NSFileManager defaultManager]
1007 contentsOfDirectoryAtPath:dir error:nil];
1008 if (! files) return 0;
1009 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
1011 for (NSString *p in files) {
1012 if ([[p pathExtension] caseInsensitiveCompare: ext])
1015 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
1018 // Get the saver name's capitalization right by reading the XML file.
1020 p = [dir stringByAppendingPathComponent: p];
1021 NSData *xmld = [NSData dataWithContentsOfFile:p];
1022 NSAssert (xmld, @"no XML: %@", p);
1023 NSString *xml = [XScreenSaverView decompressXML:xmld];
1024 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
1025 NSAssert1 (r.length, @"no name in %@", p);
1027 xml = [xml substringFromIndex: r.location + r.length];
1028 r = [xml rangeOfString:@"\"" options:0];
1029 if (r.length) name = [xml substringToIndex: r.location];
1032 # endif // USE_IPHONE
1034 NSAssert1 (name, @"no name in %@", p);
1035 if (name) [result addObject: name];
1038 if (! [result count])
1046 - (NSArray *) listSaverBundleNames
1048 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
1051 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
1052 // directories in the bundle.
1053 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
1054 stringByAppendingPathComponent:@"Contents"]
1055 stringByAppendingPathComponent:@"Resources"]];
1056 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
1058 // Also look in the same directory as the executable.
1059 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
1060 stringByDeletingLastPathComponent]];
1062 // Finally, look in standard MacOS screensaver directories.
1063 // [dirs addObject: @"~/Library/Screen Savers"];
1064 // [dirs addObject: @"/Library/Screen Savers"];
1065 // [dirs addObject: @"/System/Library/Screen Savers"];
1067 # else // USE_IPHONE
1069 // On iOS, only look in the bundle's root directory.
1070 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
1072 # endif // USE_IPHONE
1075 for (i = 0; i < [dirs count]; i++) {
1076 NSString *dir = [dirs objectAtIndex:i];
1077 NSArray *names = [self listSaverBundleNamesInDir:dir];
1078 if (! names) continue;
1079 saverDir = [dir retain];
1080 saverNames = [names retain];
1084 NSString *err = @"no .saver bundles found in: ";
1085 for (i = 0; i < [dirs count]; i++) {
1086 if (i) err = [err stringByAppendingString:@", "];
1087 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
1088 stringByAbbreviatingWithTildeInPath]];
1089 err = [err stringByAppendingString:@"/"];
1092 return [NSArray array];
1096 /* Create the popup menu of available saver names.
1100 - (NSPopUpButton *) makeMenu
1103 rect.origin.x = rect.origin.y = 0;
1104 rect.size.width = 10;
1105 rect.size.height = 10;
1106 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1109 float max_width = 0;
1110 for (i = 0; i < [saverNames count]; i++) {
1111 NSString *name = [saverNames objectAtIndex:i];
1112 [popup addItemWithTitle:name];
1113 [[popup itemWithTitle:name] setRepresentedObject:name];
1115 NSRect r = [popup frame];
1116 if (r.size.width > max_width) max_width = r.size.width;
1119 // Bind the menu to preferences, and trigger a callback when an item
1122 NSString *key = @"values.selectedSaverName";
1123 NSUserDefaultsController *prefs =
1124 [NSUserDefaultsController sharedUserDefaultsController];
1125 [prefs addObserver:self
1128 context:@selector(selectedSaverDidChange:)];
1129 [popup bind:@"selectedObject"
1133 [prefs setAppliesImmediately:YES];
1135 NSRect r = [popup frame];
1136 r.size.width = max_width;
1138 [popup autorelease];
1144 - (NSString *) makeDesc:(NSString *)saver
1145 yearOnly:(BOOL) yearp
1148 NSString *path = [saverDir stringByAppendingPathComponent:
1149 [[saver lowercaseString]
1150 stringByReplacingOccurrencesOfString:@" "
1154 path = [path stringByAppendingPathExtension:@"xml"];
1155 NSData *xmld = [NSData dataWithContentsOfFile:path];
1156 if (! xmld) goto FAIL;
1157 desc = [XScreenSaverView decompressXML:xmld];
1158 if (! desc) goto FAIL;
1160 r = [desc rangeOfString:@"<_description>"
1161 options:NSCaseInsensitiveSearch];
1162 if (r.length == 0) {
1166 desc = [desc substringFromIndex: r.location + r.length];
1167 r = [desc rangeOfString:@"</_description>"
1168 options:NSCaseInsensitiveSearch];
1170 desc = [desc substringToIndex: r.location];
1172 // Leading and trailing whitespace.
1173 desc = [desc stringByTrimmingCharactersInSet:
1174 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1176 // Let's see if we can find a year on the last line.
1177 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
1179 for (NSString *word in
1180 [[desc substringFromIndex:r.location + r.length]
1181 componentsSeparatedByCharactersInSet:
1182 [NSCharacterSet characterSetWithCharactersInString:
1184 int n = [word doubleValue];
1185 if (n > 1970 && n < 2100)
1189 // Delete everything after the first blank line.
1191 r = [desc rangeOfString:@"\n\n" options:0];
1193 desc = [desc substringToIndex: r.location];
1195 // Unwrap lines and compress whitespace.
1197 NSString *result = @"";
1198 for (NSString *s in [desc componentsSeparatedByCharactersInSet:
1199 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
1200 if ([result length] == 0)
1202 else if ([s length] > 0)
1203 result = [NSString stringWithFormat: @"%@ %@", result, s];
1209 desc = [year stringByAppendingString:
1210 [@": " stringByAppendingString: desc]];
1213 desc = year ? year : @"";
1217 if ([saverNames count] > 1)
1218 desc = @"Oops, this module appears to be incomplete.";
1226 - (NSString *) makeDesc:(NSString *)saver
1228 return [self makeDesc:saver yearOnly:NO];
1233 /* Create a dictionary of one-line descriptions of every saver,
1234 for display on the UITableView.
1236 - (NSDictionary *)makeDescTable
1238 NSMutableDictionary *dict =
1239 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1240 for (NSString *saver in saverNames) {
1241 [dict setObject:[self makeDesc:saver] forKey:saver];
1247 - (void) wantsFadeOut:(XScreenSaverView *)sender
1249 rotating_nav.view.hidden = NO; // In case it was hidden during startup.
1251 /* The XScreenSaverView screws with the status bar orientation, mostly to
1252 keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0
1253 and/or 8.2), this confuses the UINavigationController, so put the
1254 orientation back to portrait before dismissing the SaverViewController.
1257 [[UIApplication sharedApplication]
1258 setStatusBarOrientation:UIInterfaceOrientationPortrait
1262 /* Make sure the most-recently-run saver is visible. Sometimes it ends
1263 up scrolled half a line off the bottom of the screen.
1266 for (UIViewController *v in [rotating_nav viewControllers]) {
1267 if ([v isKindOfClass:[SaverListController class]]) {
1268 [(SaverListController *)v scrollTo: saverName];
1274 [rotating_nav dismissViewControllerAnimated:YES completion:^() {
1275 [nonrotating_controller release];
1276 nonrotating_controller = nil;
1277 [[rotating_nav view] becomeFirstResponder];
1282 - (void) didShake:(XScreenSaverView *)sender
1284 # if TARGET_IPHONE_SIMULATOR
1285 NSLog (@"simulating shake on saver list");
1287 [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
1292 #endif // USE_IPHONE
1296 /* This is called when the "selectedSaverName" pref changes, e.g.,
1297 when a menu selection is made.
1299 - (void)observeValueForKeyPath:(NSString *)keyPath
1301 change:(NSDictionary *)change
1302 context:(void *)context
1304 SEL dispatchSelector = (SEL)context;
1305 if (dispatchSelector != NULL) {
1306 [self performSelector:dispatchSelector withObject:change];
1308 [super observeValueForKeyPath:keyPath
1318 /* Create the desktop window shell, possibly including a preferences button.
1320 - (NSWindow *) makeWindow
1323 static int count = 0;
1324 Bool simple_p = ([saverNames count] == 1);
1326 NSPopUpButton *menu = 0;
1331 sv_rect.origin.x = sv_rect.origin.y = 0;
1332 sv_rect.size.width = 320;
1333 sv_rect.size.height = 240;
1334 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
1335 initWithFrame:sv_rect
1338 // make a "Preferences" button
1343 rect.size.width = rect.size.height = 10;
1344 pb = [[NSButton alloc] initWithFrame:rect];
1345 [pb setTitle:@"Preferences"];
1346 [pb setBezelStyle:NSRoundedBezelStyle];
1349 rect.origin.x = ([sv frame].size.width -
1350 [pb frame].size.width) / 2;
1351 [pb setFrameOrigin:rect.origin];
1355 [pb setTarget:self];
1356 [pb setAction:@selector(openPreferences:)];
1358 // Make a saver selection menu
1360 menu = [self makeMenu];
1363 [menu setFrameOrigin:rect.origin];
1365 // make a box to wrap the saverView
1369 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1370 gbox = [[NSBox alloc] initWithFrame:rect];
1371 rect.size.width = rect.size.height = 10;
1372 [gbox setContentViewMargins:rect.size];
1373 [gbox setTitlePosition:NSNoTitle];
1374 [gbox addSubview:sv];
1377 // make a box to wrap the other two boxes
1379 rect.origin.x = rect.origin.y = 0;
1380 rect.size.width = [gbox frame].size.width;
1381 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1382 pbox = [[NSBox alloc] initWithFrame:rect];
1383 [pbox setTitlePosition:NSNoTitle];
1384 [pbox setBorderType:NSNoBorder];
1385 [pbox addSubview:gbox];
1387 if (menu) [pbox addSubview:menu];
1388 if (pb) [pbox addSubview:pb];
1392 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1393 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1394 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1395 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1398 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1401 // and make a window to hold that.
1403 NSScreen *screen = [NSScreen mainScreen];
1404 rect = pbox ? [pbox frame] : [sv frame];
1405 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1406 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1408 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1410 NSWindow *win = [[NSWindow alloc]
1411 initWithContentRect:rect
1412 styleMask:(NSTitledWindowMask |
1413 NSClosableWindowMask |
1414 NSMiniaturizableWindowMask |
1415 NSResizableWindowMask)
1416 backing:NSBackingStoreBuffered
1419 [win setMinSize:[win frameRectForContentRect:rect].size];
1420 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1423 [win makeKeyAndOrderFront:win];
1425 [sv startAnimation]; // this is the dummy saver
1436 for (NSWindow *win in windows) {
1437 ScreenSaverView *sv = find_saverView ([win contentView]);
1438 if ([sv isAnimating])
1439 [sv animateOneFrame];
1443 # endif // !USE_IPHONE
1446 - (void)applicationDidFinishLaunching:
1448 (NSNotification *) notif
1449 # else // USE_IPHONE
1450 (UIApplication *) application
1451 # endif // USE_IPHONE
1453 [self listSaverBundleNames];
1455 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1458 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1459 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1464 // Create either one window (for standalone, e.g. Phosphor.app)
1465 // or two windows for SaverTester.app.
1466 for (i = 0; i < window_count; i++) {
1467 NSWindow *win = [self makeWindow];
1468 [win setDelegate:self];
1469 // Get the last-saved window position out of preferences.
1470 [win setFrameAutosaveName:
1471 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1472 [win setFrameUsingName:[win frameAutosaveName]];
1474 // This prevents clicks from being seen by savers.
1475 // [win setMovableByWindowBackground:YES];
1476 win.releasedWhenClosed = NO;
1479 # else // USE_IPHONE
1481 # undef ya_rand_init
1482 ya_rand_init (0); // Now's a good time.
1486 "You must call this method before attempting to get orientation data from
1487 the receiver. This method enables the device's accelerometer hardware
1488 and begins the delivery of acceleration events to the receiver."
1490 Adding or removing this doesn't seem to make any difference. It's
1491 probably getting called by the UINavigationController. Still... */
1492 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
1494 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1497 if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
1498 rotating_nav.view.hidden = YES;
1500 [window setRootViewController: rotating_nav];
1501 [window setAutoresizesSubviews:YES];
1502 [window setAutoresizingMask:
1503 (UIViewAutoresizingFlexibleWidth |
1504 UIViewAutoresizingFlexibleHeight)];
1506 SaverListController *menu = [[SaverListController alloc]
1507 initWithNames:saverNames
1508 descriptions:[self makeDescTable]];
1509 [rotating_nav pushViewController:menu animated:YES];
1510 [menu becomeFirstResponder];
1513 application.applicationSupportsShakeToEdit = YES;
1516 # endif // USE_IPHONE
1518 NSString *forced = 0;
1519 /* In the XCode project, each .saver scheme sets this env var when
1520 launching SaverTester.app so that it knows which one we are
1521 currently debugging. If this is set, it overrides the default
1522 selection in the popup menu. If unset, that menu persists to
1523 whatever it was last time.
1525 const char *f = getenv ("SELECTED_SAVER");
1527 forced = [NSString stringWithCString:(char *)f
1528 encoding:NSUTF8StringEncoding];
1530 if (forced && ![saverNames containsObject:forced]) {
1531 NSLog(@"forced saver \"%@\" does not exist", forced);
1535 // If there's only one saver, run that.
1536 if (!forced && [saverNames count] == 1)
1537 forced = [saverNames objectAtIndex:0];
1540 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1545 // If nothing was selected (e.g., this is the first launch)
1546 // then scroll randomly instead of starting up at "A".
1549 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1552 [menu scrollTo: prev];
1553 # endif // USE_IPHONE
1556 [prefs setObject:forced forKey:@"selectedSaverName"];
1559 /* Don't auto-launch the saver unless it was running last time.
1560 XScreenSaverView manages this, on crash_timer.
1563 if (!forced && ![prefs boolForKey:@"wasRunning"])
1567 [self selectedSaverDidChange:nil];
1568 // [NSTimer scheduledTimerWithTimeInterval: 0
1570 // selector:@selector(selectedSaverDidChange:)
1577 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1578 ScreenSaverView to run its own timer calling animateOneFrame.
1579 On 10.9, that fails because the private class ScreenSaverModule
1580 is only initialized properly by ScreenSaverEngine, and in the
1581 context of SaverRunner, the null ScreenSaverEngine instance
1582 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1583 So, if it looks like this is the 10.9 version of ScreenSaverModule
1584 instead of the 10.8 version, we run our own timer here. This sucks.
1587 Class ssm = NSClassFromString (@"ScreenSaverModule");
1588 if (ssm && [ssm instancesRespondToSelector:
1589 NSSelectorFromString(@"needsAnimationTimer")]) {
1590 NSWindow *win = [windows objectAtIndex:0];
1591 ScreenSaverView *sv = find_saverView ([win contentView]);
1592 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1593 [sv animationTimeInterval]
1595 selector:@selector(animTimer)
1600 # endif // !USE_IPHONE
1606 /* When the window closes, exit (even if prefs still open.)
1608 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1613 /* When the window is about to close, stop its animation.
1614 Without this, timers might fire after the window is dead.
1616 - (void)windowWillClose:(NSNotification *)notification
1618 NSWindow *win = [notification object];
1619 NSView *cv = win ? [win contentView] : 0;
1620 ScreenSaverView *sv = cv ? find_saverView (cv) : 0;
1621 if (sv && [sv isAnimating])
1625 # else // USE_IPHONE
1627 - (void)applicationWillResignActive:(UIApplication *)app
1629 [(XScreenSaverView *)view setScreenLocked:YES];
1632 - (void)applicationDidBecomeActive:(UIApplication *)app
1634 [(XScreenSaverView *)view setScreenLocked:NO];
1637 - (void)applicationDidEnterBackground:(UIApplication *)application
1639 [(XScreenSaverView *)view setScreenLocked:YES];
1642 #endif // USE_IPHONE