X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?p=xscreensaver;a=blobdiff_plain;f=OSX%2FSaverRunner.m;h=4481bfce0af93e7be4707e5a5cbff390da10d470;hp=797cd2cec5b289ee7cd9d86b5181da5076127cb4;hb=019de959b265701cd0c3fccbb61f2b69f06bf9ee;hpb=ec8d2b32b63649e6d32bdfb306eda062769af823 diff --git a/OSX/SaverRunner.m b/OSX/SaverRunner.m index 797cd2ce..4481bfce 100644 --- a/OSX/SaverRunner.m +++ b/OSX/SaverRunner.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski +/* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that @@ -9,7 +9,7 @@ * implied warranty. */ -/* This program serves two purposes: +/* This program serves three purposes: First, It is a test harness for screen savers. When it launches, it looks around for .saver bundles (in the current directory, and then in @@ -19,46 +19,127 @@ Second, it can be used to transform any screen saver into a standalone program. Just put one (and only one) .saver bundle into the app - bundle's Contents/PlugIns/ directory, and it will load and run that + bundle's Contents/Resources/ directory, and it will load and run that saver at start-up (without the saver-selection menu or other chrome). This is how the "Phosphor.app" and "Apple2.app" programs work. + + Third, it is the scaffolding which turns a set of screen savers into + a single iPhone / iPad program. In that case, all of the savers are + linked in to this executable, since iOS does not allow dynamic loading + of bundles that have executable code in them. Bleh. */ +#import #import "SaverRunner.h" +#import "SaverListController.h" #import "XScreenSaverGLView.h" +#import "yarandom.h" + +#ifdef USE_IPHONE + +@interface RotateyViewController : UINavigationController +@end + +@implementation RotateyViewController +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o +{ + return YES; +} +@end + +#endif // USE_IPHONE + @implementation SaverRunner + - (ScreenSaverView *) makeSaverView: (NSString *) module + withSize: (NSSize) size { + Class new_class = 0; + +# ifndef USE_IPHONE + + // Load the XScreenSaverView subclass and code from a ".saver" bundle. + NSString *name = [module stringByAppendingPathExtension:@"saver"]; NSString *path = [saverDir stringByAppendingPathComponent:name]; + + if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSLog(@"bundle \"%@\" does not exist", path); + return 0; + } + + NSLog(@"Loading %@", path); + + // NSBundle *obundle = saverBundle; + saverBundle = [NSBundle bundleWithPath:path]; - Class new_class = [saverBundle principalClass]; - NSAssert1 (new_class, @"unable to load \"%@\"", path); + if (saverBundle) + new_class = [saverBundle principalClass]; + + // Not entirely unsurprisingly, this tends to break the world. + // if (obundle && obundle != saverBundle) + // [obundle unload]; +# else // USE_IPHONE + + // Determine whether to create an X11 view or an OpenGL view by + // looking for the "gl" tag in the xml file. This is kind of awful. + + NSString *path = [saverDir + stringByAppendingPathComponent: + [[[module lowercaseString] + stringByReplacingOccurrencesOfString:@" " + withString:@""] + stringByAppendingPathExtension:@"xml"]]; + NSData *xmld = [NSData dataWithContentsOfFile:path]; + NSAssert (xmld, @"no XML: %@", path); + NSString *xml = [XScreenSaverView decompressXML:xmld]; + Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0); + + new_class = (gl_p + ? [XScreenSaverGLView class] + : [XScreenSaverView class]); + +# endif // USE_IPHONE + + if (! new_class) + return 0; NSRect rect; rect.origin.x = rect.origin.y = 0; - rect.size.width = 320; - rect.size.height = 240; + rect.size.width = size.width; + rect.size.height = size.height; - id instance = [[new_class alloc] initWithFrame:rect isPreview:YES]; - NSAssert1 (instance, @"unable to instantiate %@", new_class); + XScreenSaverView *instance = + [(XScreenSaverView *) [new_class alloc] + initWithFrame:rect + saverName:module + isPreview:YES]; + if (! instance) { + NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module); + return 0; + } /* KLUGE: Inform the underlying program that we're in "standalone" - mode. This is kind of horrible but I haven't thought of a more - sensible way to make this work. + mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver". + This is kind of horrible but I haven't thought of a more sensible + way to make this work. */ +# ifndef USE_IPHONE if ([saverNames count] == 1) { putenv (strdup ("XSCREENSAVER_STANDALONE=1")); } +# endif return (ScreenSaverView *) instance; } +#ifndef USE_IPHONE + static ScreenSaverView * find_saverView_child (NSView *v) { @@ -90,6 +171,9 @@ find_saverView (NSView *v) } +/* Changes the contents of the menubar menus to correspond to + the running saver. Desktop only. + */ static void relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) { @@ -116,7 +200,6 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) - (void) openPreferences: (id) sender { ScreenSaverView *sv; - if ([sender isKindOfClass:[NSView class]]) { // Sent from button sv = find_saverView ((NSView *) sender); } else { @@ -130,6 +213,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) } NSAssert (sv, @"no saver view"); + if (!sv) return; NSWindow *prefs = [sv configureSheet]; [NSApp beginSheet:prefs @@ -144,11 +228,13 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) ones will blow up if one re-inits but the other doesn't. */ if (code != NSCancelButton) { - [sv stopAnimation]; + if ([sv isAnimating]) + [sv stopAnimation]; [sv startAnimation]; } } + - (void) preferencesClosed: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo @@ -156,40 +242,254 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) [NSApp stopModalWithCode:returnCode]; } +#else // USE_IPHONE -- (void)loadSaver:(NSString *)name + +- (UIImage *) screenshot { - int i; - for (i = 0; i < [windows count]; i++) { - NSWindow *window = [windows objectAtIndex:i]; - NSView *cv = [window contentView]; - ScreenSaverView *old_view = find_saverView (cv); - NSView *sup = [old_view superview]; + return saved_screenshot; +} + +- (void) saveScreenshot +{ + // Most of this is from: + // http://developer.apple.com/library/ios/#qa/qa1703/_index.html + // The rotation stuff is by me. + + CGSize size = [[UIScreen mainScreen] bounds].size; + + UIInterfaceOrientation orient = + [[window rootViewController] interfaceOrientation]; + if (orient == UIInterfaceOrientationLandscapeLeft || + orient == UIInterfaceOrientationLandscapeRight) { + // Rotate the shape of the canvas 90 degrees. + double s = size.width; + size.width = size.height; + size.height = s; + } + + + // Create a graphics context with the target size + // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to + // take the scale into consideration + // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext + + if (UIGraphicsBeginImageContextWithOptions) + UIGraphicsBeginImageContextWithOptions (size, NO, 0); + else + UIGraphicsBeginImageContext (size); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + + + // Rotate the graphics context to match current hardware rotation. + // + switch (orient) { + case UIInterfaceOrientationPortraitUpsideDown: + CGContextTranslateCTM (ctx, [window center].x, [window center].y); + CGContextRotateCTM (ctx, M_PI); + CGContextTranslateCTM (ctx, -[window center].x, -[window center].y); + break; + case UIInterfaceOrientationLandscapeLeft: + case UIInterfaceOrientationLandscapeRight: + CGContextTranslateCTM (ctx, + ([window frame].size.height - + [window frame].size.width) / 2, + ([window frame].size.width - + [window frame].size.height) / 2); + CGContextTranslateCTM (ctx, [window center].x, [window center].y); + CGContextRotateCTM (ctx, + (orient == UIInterfaceOrientationLandscapeLeft + ? M_PI/2 + : -M_PI/2)); + CGContextTranslateCTM (ctx, -[window center].x, -[window center].y); + break; + default: + break; + } + + // Iterate over every window from back to front + // + for (UIWindow *win in [[UIApplication sharedApplication] windows]) { + if (![win respondsToSelector:@selector(screen)] || + [win screen] == [UIScreen mainScreen]) { + + // -renderInContext: renders in the coordinate space of the layer, + // so we must first apply the layer's geometry to the graphics context + CGContextSaveGState (ctx); + + // Center the context around the window's anchor point + CGContextTranslateCTM (ctx, [win center].x, [win center].y); + + // Apply the window's transform about the anchor point + CGContextConcatCTM (ctx, [win transform]); + + // Offset by the portion of the bounds left of and above anchor point + CGContextTranslateCTM (ctx, + -[win bounds].size.width * [[win layer] anchorPoint].x, + -[win bounds].size.height * [[win layer] anchorPoint].y); - NSString *old_title = [window title]; + // Render the layer hierarchy to the current context + [[win layer] renderInContext:ctx]; + + // Restore the context + CGContextRestoreGState (ctx); + } + } + + if (saved_screenshot) + [saved_screenshot release]; + saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain]; + + UIGraphicsEndImageContext(); +} + + +- (void) openPreferences: (NSString *) saver +{ + [self loadSaver:saver launch:NO]; + if (! saverView) return; + + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + [prefs setObject:saver forKey:@"selectedSaverName"]; + [prefs synchronize]; + + [rootViewController pushViewController: [saverView configureView] + animated:YES]; +} + + +#endif // USE_IPHONE + + + +- (void)loadSaver:(NSString *)name launch:(BOOL)launch +{ +# ifndef USE_IPHONE + + if (saverName && [saverName isEqualToString: name]) { + if (launch) + for (NSWindow *win in windows) { + ScreenSaverView *sv = find_saverView ([win contentView]); + if (![sv isAnimating]) + [sv startAnimation]; + } + return; + } + + saverName = name; + + for (NSWindow *win in windows) { + NSView *cv = [win contentView]; + NSString *old_title = [win title]; if (!old_title) old_title = @"XScreenSaver"; - [window setTitle: name]; + [win setTitle: name]; relabel_menus (menubar, old_title, name); - [old_view stopAnimation]; - [old_view removeFromSuperview]; + ScreenSaverView *old_view = find_saverView (cv); + NSView *sup = old_view ? [old_view superview] : cv; + + if (old_view) { + if ([old_view isAnimating]) + [old_view stopAnimation]; + [old_view removeFromSuperview]; + } + + NSSize size = [cv frame].size; + ScreenSaverView *new_view = [self makeSaverView:name withSize: size]; + NSAssert (new_view, @"unable to make a saver view"); - ScreenSaverView *new_view = [self makeSaverView:name]; - [new_view setFrame: [old_view frame]]; + [new_view setFrame: (old_view ? [old_view frame] : [cv frame])]; [sup addSubview: new_view]; - [window makeFirstResponder:new_view]; + [win makeFirstResponder:new_view]; [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; - [new_view startAnimation]; + [new_view retain]; + if (launch) + [new_view startAnimation]; } NSUserDefaultsController *ctl = [NSUserDefaultsController sharedUserDefaultsController]; [ctl save:self]; + +# else // USE_IPHONE + +# if TARGET_IPHONE_SIMULATOR + NSLog (@"selecting saver \"%@\"", name); +# endif + + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + [prefs setObject:name forKey:@"selectedSaverName"]; + [prefs synchronize]; + + if (saverName && [saverName isEqualToString: name]) { + if ([saverView isAnimating]) + return; + else + goto LAUNCH; + } + + saverName = name; + + if (! backgroundView) { + // This view is the parent of the XScreenSaverView, and exists only + // so that there is a black background behind it. Without this, when + // rotation is in progress, the scrolling-list window's corners show + // through in the corners. + backgroundView = [[[NSView class] alloc] initWithFrame:[window frame]]; + [backgroundView setBackgroundColor:[NSColor blackColor]]; + } + + if (saverView) { + if ([saverView isAnimating]) + [saverView stopAnimation]; + [saverView removeFromSuperview]; + [backgroundView removeFromSuperview]; + } + + NSSize size = [window frame].size; + saverView = [self makeSaverView:name withSize: size]; + + if (! saverView) { + [[[UIAlertView alloc] initWithTitle: name + message: @"Unable to load!" + delegate: nil + cancelButtonTitle: @"Bummer" + otherButtonTitles: nil] + show]; + return; + } + + [saverView setFrame: [window frame]]; + [saverView retain]; + [[NSNotificationCenter defaultCenter] + addObserver:saverView + selector:@selector(didRotate:) + name:UIDeviceOrientationDidChangeNotification object:nil]; + + LAUNCH: + if (launch) { + [self saveScreenshot]; + [window addSubview: backgroundView]; + [backgroundView addSubview: saverView]; + [saverView becomeFirstResponder]; + [saverView startAnimation]; + [self aboutPanel:nil]; + } +# endif // USE_IPHONE +} + + +- (void)loadSaver:(NSString *)name +{ + [self loadSaver:name launch:YES]; } - (void)aboutPanel:(id)sender { +# ifndef USE_IPHONE + NSDictionary *bd = [saverBundle infoDictionary]; NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20]; @@ -205,86 +505,273 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) [[NSApplication sharedApplication] orderFrontStandardAboutPanelWithOptions:d]; +# else // USE_IPHONE + + if ([saverNames count] == 1) + return; + + NSString *name = saverName; + NSString *year = [self makeDesc:saverName yearOnly:YES]; + + + CGRect frame = [saverView frame]; + CGFloat rot; + CGFloat pt1 = 24; + CGFloat pt2 = 14; + UIFont *font1 = [UIFont boldSystemFontOfSize: pt1]; + UIFont *font2 = [UIFont italicSystemFontOfSize:pt2]; + CGSize tsize1 = [name sizeWithFont:font1 + constrainedToSize:CGSizeMake(frame.size.width, + frame.size.height)]; + CGSize tsize2 = [year sizeWithFont:font2 + constrainedToSize:CGSizeMake(frame.size.width, + frame.size.height)]; + CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ? + tsize1.width : tsize2.width, + tsize1.height + tsize2.height); + + // Don't know how to find inner margin of UITextView. + CGFloat margin = 10; + tsize.width += margin * 4; + tsize.height += margin * 2; + + if ([saverView frame].size.width >= 768) + tsize.height += pt1 * 3; // extra bottom margin on iPad + + frame = CGRectMake (0, 0, tsize.width, tsize.height); + + UIInterfaceOrientation orient = + // Why are both of these wrong when starting up rotated?? + [[UIDevice currentDevice] orientation]; + // [rootViewController interfaceOrientation]; + + /* Get the text oriented properly, and move it to the bottom of the + screen, since many savers have action in the middle. + */ + switch (orient) { + case UIDeviceOrientationLandscapeRight: + rot = -M_PI/2; + frame.origin.x = ([saverView frame].size.width + - (tsize.width - tsize.height) / 2 + - tsize.height); + frame.origin.y = ([saverView frame].size.height - tsize.height) / 2; + break; + case UIDeviceOrientationLandscapeLeft: + rot = M_PI/2; + frame.origin.x = -(tsize.width - tsize.height) / 2; + frame.origin.y = ([saverView frame].size.height - tsize.height) / 2; + break; + case UIDeviceOrientationPortraitUpsideDown: + rot = M_PI; + frame.origin.x = ([saverView frame].size.width - tsize.width) / 2; + frame.origin.y = 0; + break; + default: + rot = 0; + frame.origin.x = ([saverView frame].size.width - tsize.width) / 2; + frame.origin.y = [saverView frame].size.height - tsize.height; + break; + } + + if (aboutBox) + [aboutBox removeFromSuperview]; + + aboutBox = [[UIView alloc] initWithFrame:frame]; + + aboutBox.transform = CGAffineTransformMakeRotation (rot); + aboutBox.backgroundColor = [UIColor clearColor]; + + /* There seems to be no easy way to stroke the font, so instead draw + it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add + a black shadow to each. (You'd think the shadow alone would be + enough, but there's no way to make it dark enough to be legible.) + */ + for (int i = 0; i < 5; i++) { + UITextView *textview; + int off = 1; + frame.origin.x = frame.origin.y = 0; + switch (i) { + case 0: frame.origin.x = -off; break; + case 1: frame.origin.x = off; break; + case 2: frame.origin.y = -off; break; + case 3: frame.origin.y = off; break; + } + + for (int j = 0; j < 2; j++) { + + frame.origin.y = (j == 0 ? 0 : pt1); + textview = [[UITextView alloc] initWithFrame:frame]; + textview.font = (j == 0 ? font1 : font2); + textview.text = (j == 0 ? name : year); + textview.textAlignment = UITextAlignmentCenter; + textview.showsHorizontalScrollIndicator = NO; + textview.showsVerticalScrollIndicator = NO; + textview.scrollEnabled = NO; + textview.editable = NO; + textview.userInteractionEnabled = NO; + textview.backgroundColor = [UIColor clearColor]; + textview.textColor = (i == 4 + ? [UIColor yellowColor] + : [UIColor blackColor]); + + CALayer *textLayer = (CALayer *) + [textview.layer.sublayers objectAtIndex:0]; + textLayer.shadowColor = [UIColor blackColor].CGColor; + textLayer.shadowOffset = CGSizeMake(0, 0); + textLayer.shadowOpacity = 1; + textLayer.shadowRadius = 2; + + [aboutBox addSubview:textview]; + } + } + + CABasicAnimation *anim = + [CABasicAnimation animationWithKeyPath:@"opacity"]; + anim.duration = 0.3; + anim.repeatCount = 1; + anim.autoreverses = NO; + anim.fromValue = [NSNumber numberWithFloat:0.0]; + anim.toValue = [NSNumber numberWithFloat:1.0]; + [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"]; + + [backgroundView addSubview:aboutBox]; + + if (splashTimer) + [splashTimer invalidate]; + + splashTimer = + [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2 + target:self + selector:@selector(aboutOff) + userInfo:nil + repeats:NO]; +# endif // USE_IPHONE } +# ifdef USE_IPHONE +- (void)aboutOff +{ + if (aboutBox) { + if (splashTimer) { + [splashTimer invalidate]; + splashTimer = 0; + } + CABasicAnimation *anim = + [CABasicAnimation animationWithKeyPath:@"opacity"]; + anim.duration = 0.3; + anim.repeatCount = 1; + anim.autoreverses = NO; + anim.fromValue = [NSNumber numberWithFloat: 1]; + anim.toValue = [NSNumber numberWithFloat: 0]; + anim.delegate = self; + aboutBox.layer.opacity = 0; + [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"]; + } +} +#endif // USE_IPHONE + + - (void)selectedSaverDidChange:(NSDictionary *)change { NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; NSString *name = [prefs stringForKey:@"selectedSaverName"]; + if (! name) return; + if (! [saverNames containsObject:name]) { - NSLog (@"Saver \"%@\" does not exist", name); + NSLog (@"saver \"%@\" does not exist", name); return; } - if (name) [self loadSaver: name]; + [self loadSaver: name]; } - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir { +# ifndef USE_IPHONE + NSString *ext = @"saver"; +# else + NSString *ext = @"xml"; +# endif + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dir error:nil]; if (! files) return 0; + NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1]; - int n = [files count]; - NSMutableArray *result = [NSMutableArray arrayWithCapacity: n+1]; - - int i; - for (i = 0; i < n; i++) { - NSString *p = [files objectAtIndex:i]; - if ([[p pathExtension] caseInsensitiveCompare:@"saver"]) + for (NSString *p in files) { + if ([[p pathExtension] caseInsensitiveCompare: ext]) continue; - [result addObject: [[p lastPathComponent] stringByDeletingPathExtension]]; + + NSString *name = [[p lastPathComponent] stringByDeletingPathExtension]; + +# ifdef USE_IPHONE + // Get the saver name's capitalization right by reading the XML file. + + p = [dir stringByAppendingPathComponent: p]; + NSData *xmld = [NSData dataWithContentsOfFile:p]; + NSAssert (xmld, @"no XML: %@", p); + NSString *xml = [XScreenSaverView decompressXML:xmld]; + NSRange r = [xml rangeOfString:@"_label=\"" options:0]; + NSAssert1 (r.length, @"no name in %@", p); + if (r.length) { + xml = [xml substringFromIndex: r.location + r.length]; + r = [xml rangeOfString:@"\"" options:0]; + if (r.length) name = [xml substringToIndex: r.location]; + } + +# endif // USE_IPHONE + + NSAssert1 (name, @"no name in %@", p); + if (name) [result addObject: name]; } + if (! [result count]) + result = 0; + return result; } + - (NSArray *) listSaverBundleNames { NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10]; - // First look in the bundle itself. +# ifndef USE_IPHONE + // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/" + // directories in the bundle. + [dirs addObject: [[[[NSBundle mainBundle] bundlePath] + stringByAppendingPathComponent:@"Contents"] + stringByAppendingPathComponent:@"Resources"]]; [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]]; - // Then look in the same directory as the executable. + // Also look in the same directory as the executable. [dirs addObject: [[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent]]; - // Then look in standard screensaver directories. - [dirs addObject: @"~/Library/Screen Savers"]; - [dirs addObject: @"/Library/Screen Savers"]; - [dirs addObject: @"/System/Library/Screen Savers"]; + // Finally, look in standard MacOS screensaver directories. +// [dirs addObject: @"~/Library/Screen Savers"]; +// [dirs addObject: @"/Library/Screen Savers"]; +// [dirs addObject: @"/System/Library/Screen Savers"]; + +# else // USE_IPHONE + + // On iOS, only look in the bundle's root directory. + [dirs addObject: [[NSBundle mainBundle] bundlePath]]; + +# endif // USE_IPHONE int i; for (i = 0; i < [dirs count]; i++) { NSString *dir = [dirs objectAtIndex:i]; NSArray *names = [self listSaverBundleNamesInDir:dir]; if (! names) continue; - - // Make sure this directory is on $PATH. - - const char *cdir = [dir cStringUsingEncoding:NSUTF8StringEncoding]; - const char *opath = getenv ("PATH"); - if (!opath) opath = "/bin"; // $PATH is unset when running under Shark! - char *npath = (char *) malloc (strlen (opath) + strlen (cdir) + 30); - strcpy (npath, "PATH="); - strcat (npath, cdir); - strcat (npath, ":"); - strcat (npath, opath); - if (putenv (npath)) { - perror ("putenv"); - abort(); - } - /* Don't free (npath) -- MacOS's putenv() does not copy it. */ - saverDir = [dir retain]; saverNames = [names retain]; - return names; } @@ -296,10 +783,14 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) err = [err stringByAppendingString:@"/"]; } NSLog (@"%@", err); - exit (1); + return [NSArray array]; } +/* Create the popup menu of available saver names. + */ +#ifndef USE_IPHONE + - (NSPopUpButton *) makeMenu { NSRect rect; @@ -341,6 +832,105 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) return popup; } +#else // USE_IPHONE + +- (NSString *) makeDesc:(NSString *)saver + yearOnly:(BOOL) yearp +{ + NSString *desc = 0; + NSString *path = [saverDir stringByAppendingPathComponent: + [[saver lowercaseString] + stringByReplacingOccurrencesOfString:@" " + withString:@""]]; + NSRange r; + + path = [path stringByAppendingPathExtension:@"xml"]; + NSData *xmld = [NSData dataWithContentsOfFile:path]; + if (! xmld) goto FAIL; + desc = [XScreenSaverView decompressXML:xmld]; + if (! desc) goto FAIL; + + r = [desc rangeOfString:@"<_description>" + options:NSCaseInsensitiveSearch]; + if (r.length == 0) { + desc = 0; + goto FAIL; + } + desc = [desc substringFromIndex: r.location + r.length]; + r = [desc rangeOfString:@"" + options:NSCaseInsensitiveSearch]; + if (r.length > 0) + desc = [desc substringToIndex: r.location]; + + // Leading and trailing whitespace. + desc = [desc stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + // Let's see if we can find a year on the last line. + r = [desc rangeOfString:@"\n" options:NSBackwardsSearch]; + NSString *year = 0; + for (NSString *word in + [[desc substringFromIndex:r.location + r.length] + componentsSeparatedByCharactersInSet: + [NSCharacterSet characterSetWithCharactersInString: + @" \t\n-."]]) { + int n = [word doubleValue]; + if (n > 1970 && n < 2100) + year = word; + } + + // Delete everything after the first blank line. + r = [desc rangeOfString:@"\n\n" options:0]; + if (r.length > 0) + desc = [desc substringToIndex: r.location]; + + // Truncate really long ones. + int max = 140; + if ([desc length] > max) + desc = [desc substringToIndex: max]; + + if (year) + desc = [year stringByAppendingString: + [@": " stringByAppendingString: desc]]; + + if (yearp) + desc = year ? year : @""; + +FAIL: + if (! desc) { + if ([saverNames count] > 1) + desc = @"Oops, this module appears to be incomplete."; + else + desc = @""; + } + + return desc; +} + +- (NSString *) makeDesc:(NSString *)saver +{ + return [self makeDesc:saver yearOnly:NO]; +} + + + +/* Create a dictionary of one-line descriptions of every saver, + for display on the UITableView. + */ +- (NSDictionary *)makeDescTable +{ + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithCapacity:[saverNames count]]; + for (NSString *saver in saverNames) { + [dict setObject:[self makeDesc:saver] forKey:saver]; + } + return dict; +} + + +#endif // USE_IPHONE + + /* This is called when the "selectedSaverName" pref changes, e.g., when a menu selection is made. @@ -362,6 +952,10 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) } +# ifndef USE_IPHONE + +/* Create the desktop window shell, possibly including a preferences button. + */ - (NSWindow *) makeWindow { NSRect rect; @@ -450,7 +1044,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) rect.origin.x += rect.size.width * (count ? 0.55 : -0.55); - NSWindow *window = [[NSWindow alloc] + NSWindow *win = [[NSWindow alloc] initWithContentRect:rect styleMask:(NSTitledWindowMask | NSClosableWindowMask | @@ -459,66 +1053,192 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) backing:NSBackingStoreBuffered defer:YES screen:screen]; - [window setMinSize:[window frameRectForContentRect:rect].size]; - - [[window contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)]; + [win setMinSize:[win frameRectForContentRect:rect].size]; + [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)]; - [window makeKeyAndOrderFront:window]; + [win makeKeyAndOrderFront:win]; [sv startAnimation]; // this is the dummy saver count++; - return window; + return win; } -- (void)applicationDidFinishLaunching: (NSNotification *) notif +- (void) animTimer +{ + for (NSWindow *win in windows) { + ScreenSaverView *sv = find_saverView ([win contentView]); + if ([sv isAnimating]) + [sv animateOneFrame]; + } +} + +# endif // !USE_IPHONE + + +- (void)applicationDidFinishLaunching: +# ifndef USE_IPHONE + (NSNotification *) notif +# else // USE_IPHONE + (UIApplication *) application +# endif // USE_IPHONE { [self listSaverBundleNames]; - int n = ([saverNames count] == 1 ? 1 : 2); - NSMutableArray *a = [[NSMutableArray arrayWithCapacity: n+1] retain]; +# ifndef USE_IPHONE + int window_count = ([saverNames count] <= 1 ? 1 : 2); + NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1] + retain]; windows = a; + int i; - for (i = 0; i < n; i++) { - NSWindow *window = [self makeWindow]; + // Create either one window (for standalone, e.g. Phosphor.app) + // or two windows for SaverTester.app. + for (i = 0; i < window_count; i++) { + NSWindow *win = [self makeWindow]; // Get the last-saved window position out of preferences. - [window setFrameAutosaveName: + [win setFrameAutosaveName: [NSString stringWithFormat:@"XScreenSaverWindow%d", i]]; - [window setFrameUsingName:[window frameAutosaveName]]; - [a addObject: window]; + [win setFrameUsingName:[win frameAutosaveName]]; + [a addObject: win]; + // This prevents clicks from being seen by savers. + // [win setMovableByWindowBackground:YES]; } +# else // USE_IPHONE - if (n == 1) { - [self loadSaver:[saverNames objectAtIndex:0]]; - } else { +# undef ya_rand_init + ya_rand_init (0); // Now's a good time. - /* In the XCode project, each .saver scheme sets this env var when - launching SaverTester.app so that it knows which one we are - currently debugging. If this is set, it overrides the default - selection in the popup menu. If unset, that menu persists to - whatever it was last time. - */ - const char *forced = getenv ("SELECTED_SAVER"); - if (forced && *forced) { - NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; - NSString *s = [NSString stringWithCString:(char *)forced - encoding:NSUTF8StringEncoding]; - NSLog (@"selecting saver %@", s); - [prefs setObject:s forKey:@"selectedSaverName"]; - } + rootViewController = [[[RotateyViewController alloc] init] retain]; + [window setRootViewController: rootViewController]; + + SaverListController *menu = [[SaverListController alloc] + initWithNames:saverNames + descriptions:[self makeDescTable]]; + [rootViewController pushViewController:menu animated:YES]; + [menu becomeFirstResponder]; + + [window makeKeyAndVisible]; + [window setAutoresizesSubviews:YES]; + [window setAutoresizingMask: + (UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight)]; - [self selectedSaverDidChange:nil]; + application.applicationSupportsShakeToEdit = YES; + +# endif // USE_IPHONE + + NSString *forced = 0; + /* In the XCode project, each .saver scheme sets this env var when + launching SaverTester.app so that it knows which one we are + currently debugging. If this is set, it overrides the default + selection in the popup menu. If unset, that menu persists to + whatever it was last time. + */ + const char *f = getenv ("SELECTED_SAVER"); + if (f && *f) + forced = [NSString stringWithCString:(char *)f + encoding:NSUTF8StringEncoding]; + + if (forced && ![saverNames containsObject:forced]) { + NSLog(@"forced saver \"%@\" does not exist", forced); + forced = 0; } + + // If there's only one saver, run that. + if (!forced && [saverNames count] == 1) + forced = [saverNames objectAtIndex:0]; + + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + +# ifdef USE_IPHONE + NSString *prev = [prefs stringForKey:@"selectedSaverName"]; + + if (forced) + prev = forced; + + // If nothing was selected (e.g., this is the first launch) + // then scroll randomly instead of starting up at "A". + // + if (!prev) + prev = [saverNames objectAtIndex: (random() % [saverNames count])]; + + if (prev) + [menu scrollTo: prev]; +# endif // USE_IPHONE + + if (forced) + [prefs setObject:forced forKey:@"selectedSaverName"]; + +# ifdef USE_IPHONE + /* Don't auto-launch the saver unless it was running last time. + XScreenSaverView manages this, on crash_timer. + Unless forced. + */ + if (!forced && ![prefs boolForKey:@"wasRunning"]) + return; +# endif + + [self selectedSaverDidChange:nil]; + + +# ifndef USE_IPHONE + /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the + ScreenSaverView to run its own timer calling animateOneFrame. + On 10.9, that fails because the private class ScreenSaverModule + is only initialized properly by ScreenSaverEngine, and in the + context of SaverRunner, the null ScreenSaverEngine instance + behaves as if [ScreenSaverEngine needsAnimationTimer] returned false. + So, if it looks like this is the 10.9 version of ScreenSaverModule + instead of the 10.8 version, we run our own timer here. This sucks. + */ + if (!anim_timer) { + Class ssm = NSClassFromString (@"ScreenSaverModule"); + if (ssm && [ssm instancesRespondToSelector: + @selector(needsAnimationTimer)]) { + NSWindow *win = [windows objectAtIndex:0]; + ScreenSaverView *sv = find_saverView ([win contentView]); + anim_timer = [NSTimer scheduledTimerWithTimeInterval: + [sv animationTimeInterval] + target:self + selector:@selector(animTimer) + userInfo:nil + repeats:YES]; + } + } +# endif // !USE_IPHONE } +#ifndef USE_IPHONE + /* When the window closes, exit (even if prefs still open.) -*/ + */ - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n { return YES; } +# else // USE_IPHONE + +- (void)applicationWillResignActive:(UIApplication *)app +{ + [(XScreenSaverView *)view setScreenLocked:YES]; +} + +- (void)applicationDidBecomeActive:(UIApplication *)app +{ + [(XScreenSaverView *)view setScreenLocked:NO]; +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + [(XScreenSaverView *)view setScreenLocked:YES]; +} + +#endif // USE_IPHONE + + @end