1 /* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski <jwz@jwz.org>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
12 /* This program serves three purposes:
14 First, It is a test harness for screen savers. When it launches, it
15 looks around for .saver bundles (in the current directory, and then in
16 the standard directories) and puts up a pair of windows that allow you
17 to select the saver to run. This is less clicking than running them
18 through System Preferences. This is the "SaverTester.app" program.
20 Second, it can be used to transform any screen saver into a standalone
21 program. Just put one (and only one) .saver bundle into the app
22 bundle's Contents/Resources/ directory, and it will load and run that
23 saver at start-up (without the saver-selection menu or other chrome).
24 This is how the "Phosphor.app" and "Apple2.app" programs work.
26 Third, it is the scaffolding which turns a set of screen savers into
27 a single iPhone / iPad program. In that case, all of the savers are
28 linked in to this executable, since iOS does not allow dynamic loading
29 of bundles that have executable code in them. Bleh.
32 #import <TargetConditionals.h>
33 #import "SaverRunner.h"
34 #import "SaverListController.h"
35 #import "XScreenSaverGLView.h"
40 @interface RotateyViewController : UINavigationController
46 @implementation RotateyViewController
48 /* This subclass exists so that we can ask that the SaverListController and
49 preferences panels be auto-rotated by the system. Note that the
50 XScreenSaverView is not auto-rotated because it is on a different UIWindow.
53 - (id)initWithRotation:(BOOL)rotatep
56 allowRotation = rotatep;
60 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
62 return allowRotation; /* Deprecated in iOS 6 */
65 - (BOOL)shouldAutorotate /* Added in iOS 6 */
70 - (NSUInteger)supportedInterfaceOrientations /* Added in iOS 6 */
72 return UIInterfaceOrientationMaskAll;
78 /* This subclass exists to ensure that all events on the saverWindow actually
79 go to the saverView. For some reason, the rootViewController's
80 UILayoutContainerView was capturing all of our events (touches and shakes).
83 @interface EventCapturingWindow : UIWindow
84 @property(assign) UIView *eventView;
87 @implementation EventCapturingWindow
88 @synthesize eventView;
90 /* Always deliver touch events to the eventView if we have one.
92 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
97 return [super hitTest:point withEvent:event];
100 /* Always deliver motion events to the eventView if we have one.
102 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
105 [eventView motionBegan:motion withEvent:event];
107 [super motionBegan:motion withEvent:event];
110 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
113 [eventView motionEnded:motion withEvent:event];
115 [super motionEnded:motion withEvent:event];
118 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
121 [eventView motionCancelled:motion withEvent:event];
123 [super motionCancelled:motion withEvent:event];
132 @implementation SaverRunner
135 - (ScreenSaverView *) makeSaverView: (NSString *) module
136 withSize: (NSSize) size
142 // Load the XScreenSaverView subclass and code from a ".saver" bundle.
144 NSString *name = [module stringByAppendingPathExtension:@"saver"];
145 NSString *path = [saverDir stringByAppendingPathComponent:name];
147 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
148 NSLog(@"bundle \"%@\" does not exist", path);
152 NSLog(@"Loading %@", path);
154 // NSBundle *obundle = saverBundle;
156 saverBundle = [NSBundle bundleWithPath:path];
158 new_class = [saverBundle principalClass];
160 // Not entirely unsurprisingly, this tends to break the world.
161 // if (obundle && obundle != saverBundle)
166 // Determine whether to create an X11 view or an OpenGL view by
167 // looking for the "gl" tag in the xml file. This is kind of awful.
169 NSString *path = [saverDir
170 stringByAppendingPathComponent:
171 [[[module lowercaseString]
172 stringByReplacingOccurrencesOfString:@" "
174 stringByAppendingPathExtension:@"xml"]];
175 NSData *xmld = [NSData dataWithContentsOfFile:path];
176 NSAssert (xmld, @"no XML: %@", path);
177 NSString *xml = [XScreenSaverView decompressXML:xmld];
178 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
181 ? [XScreenSaverGLView class]
182 : [XScreenSaverView class]);
184 # endif // USE_IPHONE
190 rect.origin.x = rect.origin.y = 0;
191 rect.size.width = size.width;
192 rect.size.height = size.height;
194 XScreenSaverView *instance =
195 [(XScreenSaverView *) [new_class alloc]
200 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
205 /* KLUGE: Inform the underlying program that we're in "standalone"
206 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
207 This is kind of horrible but I haven't thought of a more sensible
208 way to make this work.
211 if ([saverNames count] == 1) {
212 putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
216 return (ScreenSaverView *) instance;
222 static ScreenSaverView *
223 find_saverView_child (NSView *v)
225 NSArray *kids = [v subviews];
226 int nkids = [kids count];
228 for (i = 0; i < nkids; i++) {
229 NSObject *kid = [kids objectAtIndex:i];
230 if ([kid isKindOfClass:[ScreenSaverView class]]) {
231 return (ScreenSaverView *) kid;
233 ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
241 static ScreenSaverView *
242 find_saverView (NSView *v)
245 NSView *p = [v superview];
249 return find_saverView_child (v);
253 /* Changes the contents of the menubar menus to correspond to
254 the running saver. Desktop only.
257 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
259 if ([v isKindOfClass:[NSMenu class]]) {
260 NSMenu *m = (NSMenu *)v;
261 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
262 withString:new_str]];
263 NSArray *kids = [m itemArray];
264 int nkids = [kids count];
266 for (i = 0; i < nkids; i++) {
267 relabel_menus ([kids objectAtIndex:i], old_str, new_str);
269 } else if ([v isKindOfClass:[NSMenuItem class]]) {
270 NSMenuItem *mi = (NSMenuItem *)v;
271 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
272 withString:new_str]];
273 NSMenu *m = [mi submenu];
274 if (m) relabel_menus (m, old_str, new_str);
279 - (void) openPreferences: (id) sender
282 if ([sender isKindOfClass:[NSView class]]) { // Sent from button
283 sv = find_saverView ((NSView *) sender);
287 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar
288 w = [windows objectAtIndex:i];
289 if ([w isKeyWindow]) break;
291 sv = find_saverView ([w contentView]);
294 NSAssert (sv, @"no saver view");
296 NSWindow *prefs = [sv configureSheet];
298 [NSApp beginSheet:prefs
299 modalForWindow:[sv window]
301 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
303 int code = [NSApp runModalForWindow:prefs];
305 /* Restart the animation if the "OK" button was hit, but not if "Cancel".
306 We have to restart *both* animations, because the xlockmore-style
307 ones will blow up if one re-inits but the other doesn't.
309 if (code != NSCancelButton) {
310 if ([sv isAnimating])
317 - (void) preferencesClosed: (NSWindow *) sheet
318 returnCode: (int) returnCode
319 contextInfo: (void *) contextInfo
321 [NSApp stopModalWithCode:returnCode];
327 - (UIImage *) screenshot
329 return saved_screenshot;
332 - (void) saveScreenshot
334 // Most of this is from:
335 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
336 // The rotation stuff is by me.
338 CGSize size = [[UIScreen mainScreen] bounds].size;
340 UIInterfaceOrientation orient =
341 [[window rootViewController] interfaceOrientation];
342 if (orient == UIInterfaceOrientationLandscapeLeft ||
343 orient == UIInterfaceOrientationLandscapeRight) {
344 // Rotate the shape of the canvas 90 degrees.
345 double s = size.width;
346 size.width = size.height;
351 // Create a graphics context with the target size
352 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
353 // take the scale into consideration
354 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
356 if (UIGraphicsBeginImageContextWithOptions)
357 UIGraphicsBeginImageContextWithOptions (size, NO, 0);
359 UIGraphicsBeginImageContext (size);
361 CGContextRef ctx = UIGraphicsGetCurrentContext();
364 // Rotate the graphics context to match current hardware rotation.
367 case UIInterfaceOrientationPortraitUpsideDown:
368 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
369 CGContextRotateCTM (ctx, M_PI);
370 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
372 case UIInterfaceOrientationLandscapeLeft:
373 case UIInterfaceOrientationLandscapeRight:
374 CGContextTranslateCTM (ctx,
375 ([window frame].size.height -
376 [window frame].size.width) / 2,
377 ([window frame].size.width -
378 [window frame].size.height) / 2);
379 CGContextTranslateCTM (ctx, [window center].x, [window center].y);
380 CGContextRotateCTM (ctx,
381 (orient == UIInterfaceOrientationLandscapeLeft
384 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
390 // Iterate over every window from back to front
392 for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
393 if (![win respondsToSelector:@selector(screen)] ||
394 [win screen] == [UIScreen mainScreen]) {
396 // -renderInContext: renders in the coordinate space of the layer,
397 // so we must first apply the layer's geometry to the graphics context
398 CGContextSaveGState (ctx);
400 // Center the context around the window's anchor point
401 CGContextTranslateCTM (ctx, [win center].x, [win center].y);
403 // Apply the window's transform about the anchor point
404 CGContextConcatCTM (ctx, [win transform]);
406 // Offset by the portion of the bounds left of and above anchor point
407 CGContextTranslateCTM (ctx,
408 -[win bounds].size.width * [[win layer] anchorPoint].x,
409 -[win bounds].size.height * [[win layer] anchorPoint].y);
411 // Render the layer hierarchy to the current context
412 [[win layer] renderInContext:ctx];
414 // Restore the context
415 CGContextRestoreGState (ctx);
419 if (saved_screenshot)
420 [saved_screenshot release];
421 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
423 UIGraphicsEndImageContext();
427 - (void) openPreferences: (NSString *) saver
429 [self loadSaver:saver launch:NO];
430 if (! saverView) return;
432 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
433 [prefs setObject:saver forKey:@"selectedSaverName"];
436 [rotating_nav pushViewController: [saverView configureView]
445 - (void)loadSaver:(NSString *)name launch:(BOOL)launch
449 if (saverName && [saverName isEqualToString: name]) {
451 for (NSWindow *win in windows) {
452 ScreenSaverView *sv = find_saverView ([win contentView]);
453 if (![sv isAnimating])
461 for (NSWindow *win in windows) {
462 NSView *cv = [win contentView];
463 NSString *old_title = [win title];
464 if (!old_title) old_title = @"XScreenSaver";
465 [win setTitle: name];
466 relabel_menus (menubar, old_title, name);
468 ScreenSaverView *old_view = find_saverView (cv);
469 NSView *sup = old_view ? [old_view superview] : cv;
472 if ([old_view isAnimating])
473 [old_view stopAnimation];
474 [old_view removeFromSuperview];
477 NSSize size = [cv frame].size;
478 ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
479 NSAssert (new_view, @"unable to make a saver view");
481 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
482 [sup addSubview: new_view];
483 [win makeFirstResponder:new_view];
484 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
487 [new_view startAnimation];
490 NSUserDefaultsController *ctl =
491 [NSUserDefaultsController sharedUserDefaultsController];
496 # if TARGET_IPHONE_SIMULATOR
497 NSLog (@"selecting saver \"%@\"", name);
500 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
501 [prefs setObject:name forKey:@"selectedSaverName"];
504 /* Cacheing this screws up rotation when starting a saver twice in a row.
505 if (saverName && [saverName isEqualToString: name]) {
506 if ([saverView isAnimating])
516 if ([saverView isAnimating])
517 [saverView stopAnimation];
518 [saverView removeFromSuperview];
519 [backgroundView removeFromSuperview];
520 [[NSNotificationCenter defaultCenter] removeObserver:saverView];
524 UIScreen *screen = [UIScreen mainScreen];
528 # ifndef __IPHONE_8_0 // iOS 7 SDK or earlier
530 size = [screen bounds].size; // points, not pixels
531 scale = [screen scale]; // available in iOS 4
533 # else // iOS 8 SDK or later
535 if ([screen respondsToSelector:@selector(nativeBounds)]) {
536 size = [screen nativeBounds].size; // available in iOS 8
537 scale = 1; // nativeBounds is in pixels.
539 /* 'nativeScale' is very confusing.
542 bounds: 320x480 scale: 2
543 nativeBounds: 640x960 nativeScale: 2
545 bounds: 320x568 scale: 2
546 nativeBounds: 640x1136 nativeScale: 2
548 bounds: 768x1024 scale: 1
549 nativeBounds: 768x1024 nativeScale: 1
551 bounds: 768x1024 scale: 2
552 nativeBounds: 1536x2048 nativeScale: 2
554 bounds: 320x568 scale: 2
555 nativeBounds: 640x1136 nativeScale: 2
557 bounds: 320x568 scale: 2
558 nativeBounds: 960x1704 nativeScale: 3
560 According to a StackOverflow comment:
562 The iPhone 6+ renders internally using @3x assets at a virtual
563 resolution of 2208x1242 (with 736x414 points), then samples that down
564 for display. The same as using a scaled resolution on a Retina MacBook
565 -- it lets them hit an integral multiple for pixel assets while still
566 having e.g. 12pt text look the same size on the screen.
568 The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
569 and use @2x assets to stick to the approximately 160 points per inch
570 of all previous devices.
572 The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
573 @2.46x assets. Instead Apple uses @3x assets and scales the complete
574 output down to about 84% of its natural size.
576 In practice Apple has decided to go with more like 87%, turning the
577 1080 into 1242. No doubt that was to find something as close as
578 possible to 84% that still produced integral sizes in both directions
579 -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
580 into, say, 1286, you'd somehow need to render 2286.22 pixels
581 vertically to scale well.
585 size = [screen bounds].size; // points, not pixels
586 scale = [screen scale]; // available in iOS 4
590 size.width = ceilf (size.width / scale);
591 size.height = ceilf (size.height / scale);
594 # if TARGET_IPHONE_SIMULATOR
595 NSLog(@"screen: %.0fx%0.f",
596 [[screen currentMode] size].width,
597 [[screen currentMode] size].height);
598 NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
599 [screen bounds].size.width,
600 [screen bounds].size.height,
602 [screen scale] * [screen bounds].size.width,
603 [screen scale] * [screen bounds].size.height);
606 if ([screen respondsToSelector:@selector(nativeBounds)])
607 NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
608 [screen nativeBounds].size.width,
609 [screen nativeBounds].size.height,
610 [screen nativeScale],
611 [screen nativeBounds].size.width / [screen nativeScale],
612 [screen nativeBounds].size.height / [screen nativeScale]);
616 /* Our view must be full screen, and view sizes are measured in points,
617 not pixels. However, since our view is on a UINavigationController
618 that does not rotate, the size must be portrait-mode even if the
621 On iOS 7, [screen bounds] always returned portrait-mode values.
622 On iOS 8, it rotates. So swap as necessary.
623 On iOS 8, [screen nativeBounds] is unrotated, in pixels not points.
625 size = [screen bounds].size;
626 if (size.width > size.height) {
627 double s = size.width;
628 size.width = size.height;
632 NSLog(@"saverView: %.0fx%.0f", size.width, size.height);
633 # endif // TARGET_IPHONE_SIMULATOR
636 saverView = [self makeSaverView:name withSize:size];
639 [[[UIAlertView alloc] initWithTitle: name
640 message: @"Unable to load!"
642 cancelButtonTitle: @"Bummer"
643 otherButtonTitles: nil]
648 [[NSNotificationCenter defaultCenter]
649 addObserver:saverView
650 selector:@selector(didRotate:)
651 name:UIDeviceOrientationDidChangeNotification object:nil];
656 [self saveScreenshot];
660 f.size = [[UIScreen mainScreen] bounds].size;
661 if (f.size.width > f.size.height) { // Force portrait
662 double swap = f.size.width;
663 f.size.width = f.size.height;
664 f.size.height = swap;
666 [backgroundView setFrame:f];
667 [saverView setFrame:f];
668 [saverWindow addSubview: backgroundView];
669 [backgroundView addSubview: saverView];
670 [saverWindow setFrame:f];
671 [saverView setBackgroundColor:[NSColor blackColor]];
673 [saverWindow setHidden:NO];
674 [saverWindow makeKeyAndVisible];
675 [saverView startAnimation];
676 [self aboutPanel:nil];
678 // Tell the UILayoutContainerView to stop intercepting our events.
679 // [[saverWindow rootViewController] view].userInteractionEnabled = NO;
680 // saverView.userInteractionEnabled = YES;
682 // Tell the saverWindow that all events should go to saverView.
684 NSAssert ([saverWindow isKindOfClass:[EventCapturingWindow class]],
685 @"saverWindow is not an EventCapturingWindow");
686 ((EventCapturingWindow *) saverWindow).eventView = saverView;
688 // Doing this makes savers cut back to the list instead of fading,
689 // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
690 // [window setHidden:YES];
692 # endif // USE_IPHONE
696 - (void)loadSaver:(NSString *)name
698 [self loadSaver:name launch:YES];
702 - (void)aboutPanel:(id)sender
706 NSDictionary *bd = [saverBundle infoDictionary];
707 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
709 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
710 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
711 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"]
712 forKey:@"ApplicationVersion"];
713 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
714 [d setValue:[[NSAttributedString alloc]
715 initWithString: (NSString *)
716 [bd objectForKey:@"CFBundleGetInfoString"]]
719 [[NSApplication sharedApplication]
720 orderFrontStandardAboutPanelWithOptions:d];
723 if ([saverNames count] == 1)
726 NSString *name = saverName;
727 NSString *year = [self makeDesc:saverName yearOnly:YES];
730 CGRect frame = [saverView frame];
734 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1];
735 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
738 CGSize s = CGSizeMake(frame.size.width, frame.size.height);
739 CGSize tsize1 = [[[NSAttributedString alloc]
741 attributes:@{ NSFontAttributeName: font1 }]
742 boundingRectWithSize: s
743 options: NSStringDrawingUsesLineFragmentOrigin
745 CGSize tsize2 = [[[NSAttributedString alloc]
747 attributes:@{ NSFontAttributeName: font2 }]
748 boundingRectWithSize: s
749 options: NSStringDrawingUsesLineFragmentOrigin
751 # else // iOS 6 or Cocoa
752 CGSize tsize1 = [name sizeWithFont:font1
753 constrainedToSize:CGSizeMake(frame.size.width,
755 CGSize tsize2 = [year sizeWithFont:font2
756 constrainedToSize:CGSizeMake(frame.size.width,
760 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
761 tsize1.width : tsize2.width,
762 tsize1.height + tsize2.height);
764 tsize.width = ceilf(tsize.width);
765 tsize.height = ceilf(tsize.height);
767 // Don't know how to find inner margin of UITextView.
769 tsize.width += margin * 4;
770 tsize.height += margin * 2;
772 if ([saverView frame].size.width >= 768)
773 tsize.height += pt1 * 3; // extra bottom margin on iPad
775 frame = CGRectMake (0, 0, tsize.width, tsize.height);
777 UIInterfaceOrientation orient = [rotating_nav interfaceOrientation];
779 /* Get the text oriented properly, and move it to the bottom of the
780 screen, since many savers have action in the middle.
783 case UIDeviceOrientationLandscapeRight:
785 frame.origin.x = ([saverView frame].size.width
786 - (tsize.width - tsize.height) / 2
788 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
790 case UIDeviceOrientationLandscapeLeft:
792 frame.origin.x = -(tsize.width - tsize.height) / 2;
793 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
795 case UIDeviceOrientationPortraitUpsideDown:
797 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
802 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2;
803 frame.origin.y = [saverView frame].size.height - tsize.height;
808 [aboutBox removeFromSuperview];
810 aboutBox = [[UIView alloc] initWithFrame:frame];
812 aboutBox.transform = CGAffineTransformMakeRotation (rot);
813 aboutBox.backgroundColor = [UIColor clearColor];
815 /* There seems to be no easy way to stroke the font, so instead draw
816 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
817 a black shadow to each. (You'd think the shadow alone would be
818 enough, but there's no way to make it dark enough to be legible.)
820 for (int i = 0; i < 5; i++) {
821 UITextView *textview;
823 frame.origin.x = frame.origin.y = 0;
825 case 0: frame.origin.x = -off; break;
826 case 1: frame.origin.x = off; break;
827 case 2: frame.origin.y = -off; break;
828 case 3: frame.origin.y = off; break;
831 for (int j = 0; j < 2; j++) {
833 frame.origin.y = (j == 0 ? 0 : pt1);
834 textview = [[UITextView alloc] initWithFrame:frame];
835 textview.font = (j == 0 ? font1 : font2);
836 textview.text = (j == 0 ? name : year);
837 textview.textAlignment = NSTextAlignmentCenter;
838 textview.showsHorizontalScrollIndicator = NO;
839 textview.showsVerticalScrollIndicator = NO;
840 textview.scrollEnabled = NO;
841 textview.editable = NO;
842 textview.userInteractionEnabled = NO;
843 textview.backgroundColor = [UIColor clearColor];
844 textview.textColor = (i == 4
845 ? [UIColor yellowColor]
846 : [UIColor blackColor]);
848 CALayer *textLayer = (CALayer *)
849 [textview.layer.sublayers objectAtIndex:0];
850 textLayer.shadowColor = [UIColor blackColor].CGColor;
851 textLayer.shadowOffset = CGSizeMake(0, 0);
852 textLayer.shadowOpacity = 1;
853 textLayer.shadowRadius = 2;
855 [aboutBox addSubview:textview];
859 CABasicAnimation *anim =
860 [CABasicAnimation animationWithKeyPath:@"opacity"];
862 anim.repeatCount = 1;
863 anim.autoreverses = NO;
864 anim.fromValue = [NSNumber numberWithFloat:0.0];
865 anim.toValue = [NSNumber numberWithFloat:1.0];
866 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
868 [backgroundView addSubview:aboutBox];
871 [splashTimer invalidate];
874 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
876 selector:@selector(aboutOff)
879 # endif // USE_IPHONE
888 [splashTimer invalidate];
891 CABasicAnimation *anim =
892 [CABasicAnimation animationWithKeyPath:@"opacity"];
894 anim.repeatCount = 1;
895 anim.autoreverses = NO;
896 anim.fromValue = [NSNumber numberWithFloat: 1];
897 anim.toValue = [NSNumber numberWithFloat: 0];
898 anim.delegate = self;
899 aboutBox.layer.opacity = 0;
900 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
907 - (void)selectedSaverDidChange:(NSDictionary *)change
909 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
910 NSString *name = [prefs stringForKey:@"selectedSaverName"];
914 if (! [saverNames containsObject:name]) {
915 NSLog (@"saver \"%@\" does not exist", name);
919 [self loadSaver: name];
923 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
926 NSString *ext = @"saver";
928 NSString *ext = @"xml";
931 NSArray *files = [[NSFileManager defaultManager]
932 contentsOfDirectoryAtPath:dir error:nil];
933 if (! files) return 0;
934 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
936 for (NSString *p in files) {
937 if ([[p pathExtension] caseInsensitiveCompare: ext])
940 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
943 // Get the saver name's capitalization right by reading the XML file.
945 p = [dir stringByAppendingPathComponent: p];
946 NSData *xmld = [NSData dataWithContentsOfFile:p];
947 NSAssert (xmld, @"no XML: %@", p);
948 NSString *xml = [XScreenSaverView decompressXML:xmld];
949 NSRange r = [xml rangeOfString:@"_label=\"" options:0];
950 NSAssert1 (r.length, @"no name in %@", p);
952 xml = [xml substringFromIndex: r.location + r.length];
953 r = [xml rangeOfString:@"\"" options:0];
954 if (r.length) name = [xml substringToIndex: r.location];
957 # endif // USE_IPHONE
959 NSAssert1 (name, @"no name in %@", p);
960 if (name) [result addObject: name];
963 if (! [result count])
971 - (NSArray *) listSaverBundleNames
973 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
976 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
977 // directories in the bundle.
978 [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
979 stringByAppendingPathComponent:@"Contents"]
980 stringByAppendingPathComponent:@"Resources"]];
981 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
983 // Also look in the same directory as the executable.
984 [dirs addObject: [[[NSBundle mainBundle] bundlePath]
985 stringByDeletingLastPathComponent]];
987 // Finally, look in standard MacOS screensaver directories.
988 // [dirs addObject: @"~/Library/Screen Savers"];
989 // [dirs addObject: @"/Library/Screen Savers"];
990 // [dirs addObject: @"/System/Library/Screen Savers"];
994 // On iOS, only look in the bundle's root directory.
995 [dirs addObject: [[NSBundle mainBundle] bundlePath]];
997 # endif // USE_IPHONE
1000 for (i = 0; i < [dirs count]; i++) {
1001 NSString *dir = [dirs objectAtIndex:i];
1002 NSArray *names = [self listSaverBundleNamesInDir:dir];
1003 if (! names) continue;
1004 saverDir = [dir retain];
1005 saverNames = [names retain];
1009 NSString *err = @"no .saver bundles found in: ";
1010 for (i = 0; i < [dirs count]; i++) {
1011 if (i) err = [err stringByAppendingString:@", "];
1012 err = [err stringByAppendingString:[[dirs objectAtIndex:i]
1013 stringByAbbreviatingWithTildeInPath]];
1014 err = [err stringByAppendingString:@"/"];
1017 return [NSArray array];
1021 /* Create the popup menu of available saver names.
1025 - (NSPopUpButton *) makeMenu
1028 rect.origin.x = rect.origin.y = 0;
1029 rect.size.width = 10;
1030 rect.size.height = 10;
1031 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1034 float max_width = 0;
1035 for (i = 0; i < [saverNames count]; i++) {
1036 NSString *name = [saverNames objectAtIndex:i];
1037 [popup addItemWithTitle:name];
1038 [[popup itemWithTitle:name] setRepresentedObject:name];
1040 NSRect r = [popup frame];
1041 if (r.size.width > max_width) max_width = r.size.width;
1044 // Bind the menu to preferences, and trigger a callback when an item
1047 NSString *key = @"values.selectedSaverName";
1048 NSUserDefaultsController *prefs =
1049 [NSUserDefaultsController sharedUserDefaultsController];
1050 [prefs addObserver:self
1053 context:@selector(selectedSaverDidChange:)];
1054 [popup bind:@"selectedObject"
1058 [prefs setAppliesImmediately:YES];
1060 NSRect r = [popup frame];
1061 r.size.width = max_width;
1068 - (NSString *) makeDesc:(NSString *)saver
1069 yearOnly:(BOOL) yearp
1072 NSString *path = [saverDir stringByAppendingPathComponent:
1073 [[saver lowercaseString]
1074 stringByReplacingOccurrencesOfString:@" "
1078 path = [path stringByAppendingPathExtension:@"xml"];
1079 NSData *xmld = [NSData dataWithContentsOfFile:path];
1080 if (! xmld) goto FAIL;
1081 desc = [XScreenSaverView decompressXML:xmld];
1082 if (! desc) goto FAIL;
1084 r = [desc rangeOfString:@"<_description>"
1085 options:NSCaseInsensitiveSearch];
1086 if (r.length == 0) {
1090 desc = [desc substringFromIndex: r.location + r.length];
1091 r = [desc rangeOfString:@"</_description>"
1092 options:NSCaseInsensitiveSearch];
1094 desc = [desc substringToIndex: r.location];
1096 // Leading and trailing whitespace.
1097 desc = [desc stringByTrimmingCharactersInSet:
1098 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1100 // Let's see if we can find a year on the last line.
1101 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
1103 for (NSString *word in
1104 [[desc substringFromIndex:r.location + r.length]
1105 componentsSeparatedByCharactersInSet:
1106 [NSCharacterSet characterSetWithCharactersInString:
1108 int n = [word doubleValue];
1109 if (n > 1970 && n < 2100)
1113 // Delete everything after the first blank line.
1115 r = [desc rangeOfString:@"\n\n" options:0];
1117 desc = [desc substringToIndex: r.location];
1119 // Unwrap lines and compress whitespace.
1121 NSString *result = @"";
1122 for (NSString *s in [desc componentsSeparatedByCharactersInSet:
1123 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
1124 if ([result length] == 0)
1126 else if ([s length] > 0)
1127 result = [NSString stringWithFormat: @"%@ %@", result, s];
1133 desc = [year stringByAppendingString:
1134 [@": " stringByAppendingString: desc]];
1137 desc = year ? year : @"";
1141 if ([saverNames count] > 1)
1142 desc = @"Oops, this module appears to be incomplete.";
1150 - (NSString *) makeDesc:(NSString *)saver
1152 return [self makeDesc:saver yearOnly:NO];
1157 /* Create a dictionary of one-line descriptions of every saver,
1158 for display on the UITableView.
1160 - (NSDictionary *)makeDescTable
1162 NSMutableDictionary *dict =
1163 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1164 for (NSString *saver in saverNames) {
1165 [dict setObject:[self makeDesc:saver] forKey:saver];
1171 #endif // USE_IPHONE
1175 /* This is called when the "selectedSaverName" pref changes, e.g.,
1176 when a menu selection is made.
1178 - (void)observeValueForKeyPath:(NSString *)keyPath
1180 change:(NSDictionary *)change
1181 context:(void *)context
1183 SEL dispatchSelector = (SEL)context;
1184 if (dispatchSelector != NULL) {
1185 [self performSelector:dispatchSelector withObject:change];
1187 [super observeValueForKeyPath:keyPath
1197 /* Create the desktop window shell, possibly including a preferences button.
1199 - (NSWindow *) makeWindow
1202 static int count = 0;
1203 Bool simple_p = ([saverNames count] == 1);
1205 NSPopUpButton *menu = 0;
1210 sv_rect.origin.x = sv_rect.origin.y = 0;
1211 sv_rect.size.width = 320;
1212 sv_rect.size.height = 240;
1213 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder
1214 initWithFrame:sv_rect
1217 // make a "Preferences" button
1222 rect.size.width = rect.size.height = 10;
1223 pb = [[NSButton alloc] initWithFrame:rect];
1224 [pb setTitle:@"Preferences"];
1225 [pb setBezelStyle:NSRoundedBezelStyle];
1228 rect.origin.x = ([sv frame].size.width -
1229 [pb frame].size.width) / 2;
1230 [pb setFrameOrigin:rect.origin];
1234 [pb setTarget:self];
1235 [pb setAction:@selector(openPreferences:)];
1237 // Make a saver selection menu
1239 menu = [self makeMenu];
1242 [menu setFrameOrigin:rect.origin];
1244 // make a box to wrap the saverView
1248 rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1249 gbox = [[NSBox alloc] initWithFrame:rect];
1250 rect.size.width = rect.size.height = 10;
1251 [gbox setContentViewMargins:rect.size];
1252 [gbox setTitlePosition:NSNoTitle];
1253 [gbox addSubview:sv];
1256 // make a box to wrap the other two boxes
1258 rect.origin.x = rect.origin.y = 0;
1259 rect.size.width = [gbox frame].size.width;
1260 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1261 pbox = [[NSBox alloc] initWithFrame:rect];
1262 [pbox setTitlePosition:NSNoTitle];
1263 [pbox setBorderType:NSNoBorder];
1264 [pbox addSubview:gbox];
1265 if (menu) [pbox addSubview:menu];
1266 if (pb) [pbox addSubview:pb];
1269 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1270 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1271 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1272 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1275 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1278 // and make a window to hold that.
1280 NSScreen *screen = [NSScreen mainScreen];
1281 rect = pbox ? [pbox frame] : [sv frame];
1282 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
1283 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1285 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1287 NSWindow *win = [[NSWindow alloc]
1288 initWithContentRect:rect
1289 styleMask:(NSTitledWindowMask |
1290 NSClosableWindowMask |
1291 NSMiniaturizableWindowMask |
1292 NSResizableWindowMask)
1293 backing:NSBackingStoreBuffered
1296 [win setMinSize:[win frameRectForContentRect:rect].size];
1297 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1299 [win makeKeyAndOrderFront:win];
1301 [sv startAnimation]; // this is the dummy saver
1311 for (NSWindow *win in windows) {
1312 ScreenSaverView *sv = find_saverView ([win contentView]);
1313 if ([sv isAnimating])
1314 [sv animateOneFrame];
1318 # endif // !USE_IPHONE
1321 - (void)applicationDidFinishLaunching:
1323 (NSNotification *) notif
1324 # else // USE_IPHONE
1325 (UIApplication *) application
1326 # endif // USE_IPHONE
1328 [self listSaverBundleNames];
1331 int window_count = ([saverNames count] <= 1 ? 1 : 2);
1332 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1337 // Create either one window (for standalone, e.g. Phosphor.app)
1338 // or two windows for SaverTester.app.
1339 for (i = 0; i < window_count; i++) {
1340 NSWindow *win = [self makeWindow];
1341 // Get the last-saved window position out of preferences.
1342 [win setFrameAutosaveName:
1343 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1344 [win setFrameUsingName:[win frameAutosaveName]];
1346 // This prevents clicks from being seen by savers.
1347 // [win setMovableByWindowBackground:YES];
1349 # else // USE_IPHONE
1351 # undef ya_rand_init
1352 ya_rand_init (0); // Now's a good time.
1354 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1356 [window setRootViewController: rotating_nav];
1357 [window setAutoresizesSubviews:YES];
1358 [window setAutoresizingMask:
1359 (UIViewAutoresizingFlexibleWidth |
1360 UIViewAutoresizingFlexibleHeight)];
1362 nonrotating_nav = [[[RotateyViewController alloc] initWithRotation:NO]
1364 [nonrotating_nav setNavigationBarHidden:YES animated:NO];
1366 /* We run the saver on a different UIWindow than the one the
1367 SaverListController and preferences panels run on, because that's
1368 the only way to make rotation work right. We want the system to
1369 handle rotation of the UI stuff, but we want it to keep its hands
1370 off of rotation of the savers. As of iOS 8, this seems to be the
1371 only way to accomplish that.
1373 Also, we need to create saverWindow with a portrait rectangle, always.
1374 Note that [UIScreen bounds] returns rotated and scaled values.
1376 UIScreen *screen = [UIScreen mainScreen];
1377 # ifndef __IPHONE_8_0 // iOS 7 SDK
1378 NSRect frame = [screen bounds];
1379 int ss = [screen scale];
1381 NSRect frame = ([screen respondsToSelector:@selector(nativeBounds)]
1382 ? [screen nativeBounds] // iOS 8
1383 : [screen bounds]); // iOS 7
1384 int ss = ([screen respondsToSelector:@selector(nativeScale)]
1385 ? [screen nativeScale] // iOS 8
1386 : [screen scale]); // iOS 7
1387 # endif // iOS 8 SDK
1388 frame.size.width /= ss;
1389 frame.size.height /= ss;
1390 saverWindow = [[EventCapturingWindow alloc] initWithFrame:frame];
1391 [saverWindow setRootViewController: nonrotating_nav];
1392 [saverWindow setHidden:YES];
1394 /* This view is the parent of the XScreenSaverView, and exists only
1395 so that there is a black background behind it. Without this, when
1396 rotation is in progress, the scrolling-list window's corners show
1397 through in the corners.
1399 backgroundView = [[[NSView class] alloc] initWithFrame:[saverWindow frame]];
1400 [backgroundView setBackgroundColor:[NSColor blackColor]];
1402 SaverListController *menu = [[SaverListController alloc]
1403 initWithNames:saverNames
1404 descriptions:[self makeDescTable]];
1405 [rotating_nav pushViewController:menu animated:YES];
1406 [menu becomeFirstResponder];
1408 application.applicationSupportsShakeToEdit = YES;
1411 # endif // USE_IPHONE
1413 NSString *forced = 0;
1414 /* In the XCode project, each .saver scheme sets this env var when
1415 launching SaverTester.app so that it knows which one we are
1416 currently debugging. If this is set, it overrides the default
1417 selection in the popup menu. If unset, that menu persists to
1418 whatever it was last time.
1420 const char *f = getenv ("SELECTED_SAVER");
1422 forced = [NSString stringWithCString:(char *)f
1423 encoding:NSUTF8StringEncoding];
1425 if (forced && ![saverNames containsObject:forced]) {
1426 NSLog(@"forced saver \"%@\" does not exist", forced);
1430 // If there's only one saver, run that.
1431 if (!forced && [saverNames count] == 1)
1432 forced = [saverNames objectAtIndex:0];
1434 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1437 NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1442 // If nothing was selected (e.g., this is the first launch)
1443 // then scroll randomly instead of starting up at "A".
1446 prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1449 [menu scrollTo: prev];
1450 # endif // USE_IPHONE
1453 [prefs setObject:forced forKey:@"selectedSaverName"];
1456 /* Don't auto-launch the saver unless it was running last time.
1457 XScreenSaverView manages this, on crash_timer.
1460 if (!forced && ![prefs boolForKey:@"wasRunning"])
1464 [self selectedSaverDidChange:nil];
1465 // [NSTimer scheduledTimerWithTimeInterval: 0
1467 // selector:@selector(selectedSaverDidChange:)
1474 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1475 ScreenSaverView to run its own timer calling animateOneFrame.
1476 On 10.9, that fails because the private class ScreenSaverModule
1477 is only initialized properly by ScreenSaverEngine, and in the
1478 context of SaverRunner, the null ScreenSaverEngine instance
1479 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1480 So, if it looks like this is the 10.9 version of ScreenSaverModule
1481 instead of the 10.8 version, we run our own timer here. This sucks.
1484 Class ssm = NSClassFromString (@"ScreenSaverModule");
1485 if (ssm && [ssm instancesRespondToSelector:
1486 @selector(needsAnimationTimer)]) {
1487 NSWindow *win = [windows objectAtIndex:0];
1488 ScreenSaverView *sv = find_saverView ([win contentView]);
1489 anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1490 [sv animationTimeInterval]
1492 selector:@selector(animTimer)
1497 # endif // !USE_IPHONE
1503 /* When the window closes, exit (even if prefs still open.)
1505 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1510 # else // USE_IPHONE
1512 - (void)applicationWillResignActive:(UIApplication *)app
1514 [(XScreenSaverView *)view setScreenLocked:YES];
1517 - (void)applicationDidBecomeActive:(UIApplication *)app
1519 [(XScreenSaverView *)view setScreenLocked:NO];
1522 - (void)applicationDidEnterBackground:(UIApplication *)application
1524 [(XScreenSaverView *)view setScreenLocked:YES];
1527 #endif // USE_IPHONE