X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=OSX%2FSaverRunner.m;h=f358fb754500f5c27cb3bf78404163b23beed1db;hb=39809ded547bdbb08207d3e514950425215b4410;hp=80031e775f6456b895cda3c6da664462e5a4317c;hpb=d5186197bc394e10a4402f7f6d23fbb14103bc50;p=xscreensaver diff --git a/OSX/SaverRunner.m b/OSX/SaverRunner.m index 80031e77..f358fb75 100644 --- a/OSX/SaverRunner.m +++ b/OSX/SaverRunner.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski +/* xscreensaver, Copyright (c) 2006-2017 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 @@ -37,6 +37,16 @@ #ifdef USE_IPHONE +# ifndef __IPHONE_8_0 +# define UIInterfaceOrientationUnknown UIDeviceOrientationUnknown +# endif +# ifndef NSFoundationVersionNumber_iOS_7_1 +# define NSFoundationVersionNumber_iOS_7_1 1047.25 +# endif +# ifndef NSFoundationVersionNumber_iOS_8_0 +# define NSFoundationVersionNumber_iOS_8_0 1134.10 +# endif + @interface RotateyViewController : UINavigationController { BOOL allowRotation; @@ -67,20 +77,403 @@ return allowRotation; } -- (NSUInteger)supportedInterfaceOrientations /* Added in iOS 6 */ +- (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */ { return UIInterfaceOrientationMaskAll; } @end + +@implementation SaverViewController + +@synthesize saverName; + +- (id)initWithSaverRunner:(SaverRunner *)parent + showAboutBox:(BOOL)showAboutBox +{ + self = [super init]; + if (self) { + _parent = parent; + // _storedOrientation = UIInterfaceOrientationUnknown; + _showAboutBox = showAboutBox; + + self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + +# ifndef __IPHONE_7_0 + self.wantsFullScreenLayout = YES; // Deprecated as of iOS 7 +# endif + } + return self; +} + +- (BOOL) prefersStatusBarHidden +{ + // Requires UIViewControllerBasedStatusBarAppearance = true in plist + return YES; +} + +- (void)dealloc +{ + [_saverName release]; + // iOS: When a UIView deallocs, it doesn't do [UIView removeFromSuperView] + // for its subviews, so the subviews end up with a dangling pointer in their + // superview properties. + [aboutBox removeFromSuperview]; + [aboutBox release]; + [_saverView removeFromSuperview]; + [_saverView release]; + [super dealloc]; +} + + +- (void)loadView +{ + // The UIViewController's view must never change, so it gets set here to + // a plain black background. + + // This background view doesn't block the status bar, but that's probably + // OK, because it's never on screen for more than a fraction of a second. + UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull]; + backgroundView.backgroundColor = [UIColor blackColor]; + self.view = backgroundView; + [backgroundView release]; +} + + +- (void)aboutPanel:(UIView *)saverView + orientation:(UIInterfaceOrientation)orient +{ + if (!_showAboutBox) + return; + + NSString *name = _saverName; + NSString *year = [_parent 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]; + +# ifdef __IPHONE_7_0 + CGSize s = CGSizeMake(frame.size.width, frame.size.height); + CGSize tsize1 = [[[NSAttributedString alloc] + initWithString: name + attributes:@{ NSFontAttributeName: font1 }] + boundingRectWithSize: s + options: NSStringDrawingUsesLineFragmentOrigin + context: nil].size; + CGSize tsize2 = [[[NSAttributedString alloc] + initWithString: name + attributes:@{ NSFontAttributeName: font2 }] + boundingRectWithSize: s + options: NSStringDrawingUsesLineFragmentOrigin + context: nil].size; +# else // iOS 6 or Cocoa + 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)]; +# endif + + CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ? + tsize1.width : tsize2.width, + tsize1.height + tsize2.height); + + tsize.width = ceilf(tsize.width); + tsize.height = ceilf(tsize.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); + + /* 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 UIInterfaceOrientationLandscapeLeft: + 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 UIInterfaceOrientationLandscapeRight: + rot = M_PI/2; + frame.origin.x = -(tsize.width - tsize.height) / 2; + frame.origin.y = ([saverView frame].size.height - tsize.height) / 2; + break; + case UIInterfaceOrientationPortraitUpsideDown: + 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 release]; + } + + 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 = NSTextAlignmentCenter; + 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"]; + + [saverView addSubview:aboutBox]; + + if (splashTimer) + [splashTimer invalidate]; + + splashTimer = + [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2 + target:self + selector:@selector(aboutOff) + userInfo:nil + repeats:NO]; +} + + +- (void)aboutOff +{ + [self aboutOff:FALSE]; +} + +- (void)aboutOff:(BOOL)fast +{ + if (aboutBox) { + if (splashTimer) { + [splashTimer invalidate]; + splashTimer = 0; + } + if (fast) { + aboutBox.layer.opacity = 0; + return; + } + + 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"]; + } +} + + +- (void)createSaverView +{ + UIView *parentView = self.view; + + if (_saverView) { + [_saverView removeFromSuperview]; + [_saverView release]; + } + +# if 0 + if (_storedOrientation != UIInterfaceOrientationUnknown) { + [[UIApplication sharedApplication] + setStatusBarOrientation:_storedOrientation + animated:NO]; + } +# endif + + _saverView = [_parent newSaverView:_saverName + withSize:parentView.bounds.size]; + + if (! _saverView) { + UIAlertController *c = [UIAlertController + alertControllerWithTitle:@"Unable to load!" + message:@"" + preferredStyle:UIAlertControllerStyleAlert]; + [c addAction: [UIAlertAction actionWithTitle: @"Bummer" + style: UIAlertActionStyleDefault + handler: ^(UIAlertAction *a) { + // #### Should expose the SaverListController... + }]]; + [self presentViewController:c animated:YES completion:nil]; + + return; + } + + _saverView.delegate = _parent; + _saverView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + [self.view addSubview:_saverView]; + + // The first responder must be set only after the view was placed in the view + // heirarchy. + [_saverView becomeFirstResponder]; // For shakes on iOS 6. + [_saverView startAnimation]; + [self aboutPanel:_saverView + orientation:/* _storedOrientation */ UIInterfaceOrientationPortrait]; +} + + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self createSaverView]; +} + + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o +{ + return NO; /* Deprecated in iOS 6 */ +} + + +- (BOOL)shouldAutorotate /* Added in iOS 6 */ +{ + return + NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 ? + ![_saverView suppressRotationAnimation] : + YES; +} + + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */ +{ + // Lies from the iOS docs: + // "This method is only called if the view controller's shouldAutorotate + // method returns YES." + return UIInterfaceOrientationMaskAll; +} + + +/* +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation +{ + return UIInterfaceOrientationPortrait; +} +*/ + + +- (void)setSaverName:(NSString *)name +{ + [name retain]; + [_saverName release]; + _saverName = name; + // _storedOrientation = + // [UIApplication sharedApplication].statusBarOrientation; + + if (_saverView) + [self createSaverView]; +} + + +- (void)viewWillTransitionToSize: (CGSize)size + withTransitionCoordinator: + (id) coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + + if (!_saverView) + return; + + [CATransaction begin]; + + // Completely suppress the rotation animation, since we + // will not (visually) be rotating at all. + if ([_saverView suppressRotationAnimation]) + [CATransaction setDisableActions:YES]; + + [self aboutOff:TRUE]; // It does goofy things if we rotate while it's up + + [coordinator animateAlongsideTransition:^ + (id context) { + // This executes repeatedly during the rotation. + } completion:^(id context) { + // This executes once when the rotation has finished. + [CATransaction commit]; + [_saverView orientationChanged]; + }]; + // No code goes here, as it would execute before the above completes. +} + +@end + #endif // USE_IPHONE @implementation SaverRunner -- (ScreenSaverView *) makeSaverView: (NSString *) module +- (XScreenSaverView *) newSaverView: (NSString *) module withSize: (NSSize) size { Class new_class = 0; @@ -157,11 +550,11 @@ */ # ifndef USE_IPHONE if ([saverNames count] == 1) { - putenv (strdup ("XSCREENSAVER_STANDALONE=1")); + setenv ("XSCREENSAVER_STANDALONE", "1", 1); } # endif - return (ScreenSaverView *) instance; + return (XScreenSaverView *) instance; } @@ -171,8 +564,8 @@ static ScreenSaverView * find_saverView_child (NSView *v) { NSArray *kids = [v subviews]; - int nkids = [kids count]; - int i; + NSUInteger nkids = [kids count]; + NSUInteger i; for (i = 0; i < nkids; i++) { NSObject *kid = [kids objectAtIndex:i]; if ([kid isKindOfClass:[ScreenSaverView class]]) { @@ -209,8 +602,8 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str withString:new_str]]; NSArray *kids = [m itemArray]; - int nkids = [kids count]; - int i; + NSUInteger nkids = [kids count]; + NSUInteger i; for (i = 0; i < nkids; i++) { relabel_menus ([kids objectAtIndex:i], old_str, new_str); } @@ -230,7 +623,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) if ([sender isKindOfClass:[NSView class]]) { // Sent from button sv = find_saverView ((NSView *) sender); } else { - int i; + long i; NSWindow *w = 0; for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar w = [windows objectAtIndex:i]; @@ -248,7 +641,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) modalDelegate:self didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:) contextInfo:nil]; - int code = [NSApp runModalForWindow:prefs]; + NSUInteger code = [NSApp runModalForWindow:prefs]; /* Restart the animation if the "OK" button was hit, but not if "Cancel". We have to restart *both* animations, because the xlockmore-style @@ -285,8 +678,13 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) CGSize size = [[UIScreen mainScreen] bounds].size; - UIInterfaceOrientation orient = - [[window rootViewController] interfaceOrientation]; + // iOS 7: Needs to be [[window rootViewController] interfaceOrientation]. + // iOS 8: Needs to be UIInterfaceOrientationPortrait. + // (interfaceOrientation deprecated in iOS 8) + + UIInterfaceOrientation orient = UIInterfaceOrientationPortrait; + /* iOS 8 broke -[UIScreen bounds]. */ + if (orient == UIInterfaceOrientationLandscapeLeft || orient == UIInterfaceOrientationLandscapeRight) { // Rotate the shape of the canvas 90 degrees. @@ -301,10 +699,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) // take the scale into consideration // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext - if (UIGraphicsBeginImageContextWithOptions) - UIGraphicsBeginImageContextWithOptions (size, NO, 0); - else - UIGraphicsBeginImageContext (size); + UIGraphicsBeginImageContextWithOptions (size, NO, 0); CGContextRef ctx = UIGraphicsGetCurrentContext(); @@ -374,7 +769,8 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) - (void) openPreferences: (NSString *) saver { - [self loadSaver:saver launch:NO]; + XScreenSaverView *saverView = [self newSaverView:saver + withSize:CGSizeMake(0, 0)]; if (! saverView) return; NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; @@ -390,17 +786,16 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) -- (void)loadSaver:(NSString *)name launch:(BOOL)launch +- (void)loadSaver:(NSString *)name { # 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]; - } + for (NSWindow *win in windows) { + ScreenSaverView *sv = find_saverView ([win contentView]); + if (![sv isAnimating]) + [sv startAnimation]; + } return; } @@ -423,16 +818,15 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) } NSSize size = [cv frame].size; - ScreenSaverView *new_view = [self makeSaverView:name withSize: size]; + ScreenSaverView *new_view = [self newSaverView:name withSize: size]; NSAssert (new_view, @"unable to make a saver view"); [new_view setFrame: (old_view ? [old_view frame] : [cv frame])]; [sup addSubview: new_view]; [win makeFirstResponder:new_view]; [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; - [new_view retain]; - if (launch) - [new_view startAnimation]; + [new_view startAnimation]; + [new_view release]; } NSUserDefaultsController *ctl = @@ -441,7 +835,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) # else // USE_IPHONE -# if TARGET_IPHONE_SIMULATOR +# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR NSLog (@"selecting saver \"%@\"", name); # endif @@ -449,106 +843,120 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) [prefs setObject:name forKey:@"selectedSaverName"]; [prefs synchronize]; +/* Cacheing this screws up rotation when starting a saver twice in a row. if (saverName && [saverName isEqualToString: name]) { if ([saverView isAnimating]) return; else goto LAUNCH; } +*/ saverName = name; - if (saverView) { - if ([saverView isAnimating]) - [saverView stopAnimation]; - [saverView removeFromSuperview]; - [backgroundView removeFromSuperview]; - [[NSNotificationCenter defaultCenter] removeObserver:saverView]; - [saverView release]; + if (nonrotating_controller) { + nonrotating_controller.saverName = name; + return; } - /* We can't just use [window bounds] because that is the *rotated* rectangle - and we need the *unrotated* rectangle, so that the view is always created - in portrait orientation. Without that, the initial rotation event that - takes us from unknown->landscape will be out of step with reality. - */ +# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR UIScreen *screen = [UIScreen mainScreen]; -# ifndef __IPHONE_8_0 // iOS 7 SDK - NSSize size = [screen bounds].size; - int ss = [screen scale]; -# else // iOS 8 SDK - NSSize size = ([screen respondsToSelector:@selector(nativeBounds)] - ? [screen nativeBounds].size // iOS 8 - : [screen bounds].size); // iOS 7 - int ss = ([screen respondsToSelector:@selector(nativeScale)] - ? [screen nativeScale] // iOS 8 - : [screen scale]); // iOS 7 -# endif // iOS 8 SDK - - size.width /= ss; - size.height /= ss; - saverView = [self makeSaverView:name withSize:size]; - - if (! saverView) { - [[[UIAlertView alloc] initWithTitle: name - message: @"Unable to load!" - delegate: nil - cancelButtonTitle: @"Bummer" - otherButtonTitles: nil] - show]; - return; - } - [[NSNotificationCenter defaultCenter] - addObserver:saverView - selector:@selector(didRotate:) - name:UIDeviceOrientationDidChangeNotification object:nil]; - - LAUNCH: - - if (launch) { - [self saveScreenshot]; - NSRect f = [saverWindow bounds]; - [backgroundView setFrame:f]; - [saverView setFrame:f]; - [saverWindow addSubview: backgroundView]; - [backgroundView addSubview: saverView]; - [saverView setBackgroundColor:[NSColor blackColor]]; - - /* WTF! Without creating and keying this window, we get no events - delivered on the saverView/saverWindow! Bad craziness. - */ - { - UIWindow *dummy = [[UIWindow alloc] initWithFrame:CGRectMake(0,0,0,0)]; - [dummy setRootViewController: nonrotating_nav]; // Must be this one. - [dummy setHidden:NO]; // required - [dummy setHidden:YES]; - [dummy release]; - } + /* 'nativeScale' is very confusing. + + iPhone 4s: + bounds: 320x480 scale: 2 + nativeBounds: 640x960 nativeScale: 2 + iPhone 5s: + bounds: 320x568 scale: 2 + nativeBounds: 640x1136 nativeScale: 2 + iPad 2: + bounds: 768x1024 scale: 1 + nativeBounds: 768x1024 nativeScale: 1 + iPad Retina/Air: + bounds: 768x1024 scale: 2 + nativeBounds: 1536x2048 nativeScale: 2 + iPhone 6: + bounds: 320x568 scale: 2 + nativeBounds: 640x1136 nativeScale: 2 + iPhone 6+: + bounds: 320x568 scale: 2 + nativeBounds: 960x1704 nativeScale: 3 + + According to a StackOverflow comment: + + The iPhone 6+ renders internally using @3x assets at a virtual + resolution of 2208x1242 (with 736x414 points), then samples that down + for display. The same as using a scaled resolution on a Retina MacBook + -- it lets them hit an integral multiple for pixel assets while still + having e.g. 12pt text look the same size on the screen. + + The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch, + and use @2x assets to stick to the approximately 160 points per inch + of all previous devices. + + The 6+ is 401 pixels per inch. So it'd hypothetically need roughly + @2.46x assets. Instead Apple uses @3x assets and scales the complete + output down to about 84% of its natural size. + + In practice Apple has decided to go with more like 87%, turning the + 1080 into 1242. No doubt that was to find something as close as + possible to 84% that still produced integral sizes in both directions + -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080 + into, say, 1286, you'd somehow need to render 2286.22 pixels + vertically to scale well. + */ - [saverWindow setHidden:NO]; - [saverWindow makeKeyAndVisible]; - [saverView startAnimation]; - [self aboutPanel:nil]; + NSLog(@"screen: %.0fx%0.f", + [[screen currentMode] size].width, + [[screen currentMode] size].height); + NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f", + [screen bounds].size.width, + [screen bounds].size.height, + [screen scale], + [screen scale] * [screen bounds].size.width, + [screen scale] * [screen bounds].size.height); + +# ifdef __IPHONE_8_0 + if ([screen respondsToSelector:@selector(nativeBounds)]) + NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f", + [screen nativeBounds].size.width, + [screen nativeBounds].size.height, + [screen nativeScale], + [screen nativeBounds].size.width / [screen nativeScale], + [screen nativeBounds].size.height / [screen nativeScale]); +# endif +# endif // TARGET_IPHONE_SIMULATOR - // Doing this makes savers cut back to the list instead of fading, - // even though [XScreenSaverView stopAndClose] does setHidden:NO first. - // [window setHidden:YES]; - } -# endif // USE_IPHONE -} + // Take the screen shot before creating the screen saver view, because this + // can screw with the layout. + [self saveScreenshot]; + // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal + // presentation full screen. + rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen; -- (void)loadSaver:(NSString *)name -{ - [self loadSaver:name launch:YES]; + nonrotating_controller = [[SaverViewController alloc] + initWithSaverRunner:self + showAboutBox:[saverNames count] != 1]; + nonrotating_controller.saverName = name; + + /* LAUNCH: */ + + [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil]; + + // Doing this makes savers cut back to the list instead of fading, + // even though [XScreenSaverView stopAndClose] does setHidden:NO first. + // [window setHidden:YES]; + +# endif // USE_IPHONE } +#ifndef USE_IPHONE + - (void)aboutPanel:(id)sender { -# ifndef USE_IPHONE - NSDictionary *bd = [saverBundle infoDictionary]; NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20]; @@ -557,196 +965,17 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] forKey:@"ApplicationVersion"]; [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"]; - [d setValue:[[NSAttributedString alloc] - initWithString: (NSString *) - [bd objectForKey:@"CFBundleGetInfoString"]] - forKey:@"Credits"]; - + NSAttributedString *s = [[NSAttributedString alloc] + initWithString: (NSString *) + [bd objectForKey:@"CFBundleGetInfoString"]]; + [d setValue:s forKey:@"Credits"]; + [s release]; + [[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]; - -# ifdef __IPHONE_7_0 - CGSize s = CGSizeMake(frame.size.width, frame.size.height); - CGSize tsize1 = [[[NSAttributedString alloc] - initWithString: name - attributes:@{ NSFontAttributeName: font1 }] - boundingRectWithSize: s - options: NSStringDrawingUsesLineFragmentOrigin - context: nil].size; - CGSize tsize2 = [[[NSAttributedString alloc] - initWithString: name - attributes:@{ NSFontAttributeName: font2 }] - boundingRectWithSize: s - options: NSStringDrawingUsesLineFragmentOrigin - context: nil].size; -# else // iOS 6 or Cocoa - 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)]; -#endif - - CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ? - tsize1.width : tsize2.width, - tsize1.height + tsize2.height); - - tsize.width = ceilf(tsize.width); - tsize.height = ceilf(tsize.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 = [rotating_nav 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 = NSTextAlignmentCenter; - 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 +#endif // !USE_IPHONE @@ -906,6 +1135,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) NSRect r = [popup frame]; r.size.width = max_width; [popup setFrame:r]; + [popup autorelease]; return popup; } @@ -1014,6 +1244,51 @@ FAIL: } +- (void) wantsFadeOut:(XScreenSaverView *)sender +{ + rotating_nav.view.hidden = NO; // In case it was hidden during startup. + + /* The XScreenSaverView screws with the status bar orientation, mostly to + keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0 + and/or 8.2), this confuses the UINavigationController, so put the + orientation back to portrait before dismissing the SaverViewController. + */ +# if 0 + [[UIApplication sharedApplication] + setStatusBarOrientation:UIInterfaceOrientationPortrait + animated:NO]; +# endif + + /* Make sure the most-recently-run saver is visible. Sometimes it ends + up scrolled half a line off the bottom of the screen. + */ + if (saverName) { + for (UIViewController *v in [rotating_nav viewControllers]) { + if ([v isKindOfClass:[SaverListController class]]) { + [(SaverListController *)v scrollTo: saverName]; + break; + } + } + } + + [rotating_nav dismissViewControllerAnimated:YES completion:^() { + [nonrotating_controller release]; + nonrotating_controller = nil; + [[rotating_nav view] becomeFirstResponder]; + }]; +} + + +- (void) didShake:(XScreenSaverView *)sender +{ +# if TARGET_IPHONE_SIMULATOR + NSLog (@"simulating shake on saver list"); +# endif + [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake + withEvent: nil]; +} + + #endif // USE_IPHONE @@ -1108,8 +1383,10 @@ FAIL: [pbox setTitlePosition:NSNoTitle]; [pbox setBorderType:NSNoBorder]; [pbox addSubview:gbox]; + [gbox release]; if (menu) [pbox addSubview:menu]; if (pb) [pbox addSubview:pb]; + [pb release]; [pbox sizeToFit]; [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin]; @@ -1139,12 +1416,14 @@ FAIL: backing:NSBackingStoreBuffered defer:YES screen:screen]; - [win setMinSize:[win frameRectForContentRect:rect].size]; +// [win setMinSize:[win frameRectForContentRect:rect].size]; [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)]; + [pbox release]; [win makeKeyAndOrderFront:win]; [sv startAnimation]; // this is the dummy saver + [sv autorelease]; count++; @@ -1173,6 +1452,8 @@ FAIL: { [self listSaverBundleNames]; + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + # ifndef USE_IPHONE int window_count = ([saverNames count] <= 1 ? 1 : 2); NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1] @@ -1184,6 +1465,7 @@ FAIL: // or two windows for SaverTester.app. for (i = 0; i < window_count; i++) { NSWindow *win = [self makeWindow]; + [win setDelegate:self]; // Get the last-saved window position out of preferences. [win setFrameAutosaveName: [NSString stringWithFormat:@"XScreenSaverWindow%d", i]]; @@ -1191,65 +1473,42 @@ FAIL: [a addObject: win]; // This prevents clicks from being seen by savers. // [win setMovableByWindowBackground:YES]; + win.releasedWhenClosed = NO; + [win release]; } # else // USE_IPHONE # undef ya_rand_init ya_rand_init (0); // Now's a good time. + + /* iOS docs say: + "You must call this method before attempting to get orientation data from + the receiver. This method enables the device's accelerometer hardware + and begins the delivery of acceleration events to the receiver." + + Adding or removing this doesn't seem to make any difference. It's + probably getting called by the UINavigationController. Still... */ + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES] retain]; + + if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup. + rotating_nav.view.hidden = YES; + [window setRootViewController: rotating_nav]; [window setAutoresizesSubviews:YES]; [window setAutoresizingMask: (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; - nonrotating_nav = [[[RotateyViewController alloc] initWithRotation:NO] - retain]; - [nonrotating_nav setNavigationBarHidden:YES animated:NO]; - - /* We run the saver on a different UIWindow than the one the - SaverListController and preferences panels run on, because that's - the only way to make rotation work right. We want the system to - handle rotation of the UI stuff, but we want it to keep its hands - off of rotation of the savers. As of iOS 8, this seems to be the - only way to accomplish that. - - Also, we need to create saverWindow with a portrait rectangle, always. - Note that [UIScreen bounds] returns rotated and scaled values. - */ - UIScreen *screen = [UIScreen mainScreen]; -# ifndef __IPHONE_8_0 // iOS 7 SDK - NSRect frame = [screen bounds]; - int ss = [screen scale]; -# else // iOS 8 SDK - NSRect frame = ([screen respondsToSelector:@selector(nativeBounds)] - ? [screen nativeBounds] // iOS 8 - : [screen bounds]); // iOS 7 - int ss = ([screen respondsToSelector:@selector(nativeScale)] - ? [screen nativeScale] // iOS 8 - : [screen scale]); // iOS 7 -# endif // iOS 8 SDK - frame.size.width /= ss; - frame.size.height /= ss; - saverWindow = [[UIWindow alloc] initWithFrame:frame]; - [saverWindow setRootViewController: nonrotating_nav]; - [saverWindow setHidden:YES]; - - /* 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:[saverWindow frame]]; - [backgroundView setBackgroundColor:[NSColor blackColor]]; - SaverListController *menu = [[SaverListController alloc] initWithNames:saverNames descriptions:[self makeDescTable]]; [rotating_nav pushViewController:menu animated:YES]; [menu becomeFirstResponder]; + [menu autorelease]; application.applicationSupportsShakeToEdit = YES; @@ -1277,8 +1536,6 @@ FAIL: if (!forced && [saverNames count] == 1) forced = [saverNames objectAtIndex:0]; - NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; - # ifdef USE_IPHONE NSString *prev = [prefs stringForKey:@"selectedSaverName"]; @@ -1329,7 +1586,7 @@ FAIL: if (!anim_timer) { Class ssm = NSClassFromString (@"ScreenSaverModule"); if (ssm && [ssm instancesRespondToSelector: - @selector(needsAnimationTimer)]) { + NSSelectorFromString(@"needsAnimationTimer")]) { NSWindow *win = [windows objectAtIndex:0]; ScreenSaverView *sv = find_saverView ([win contentView]); anim_timer = [NSTimer scheduledTimerWithTimeInterval: @@ -1353,6 +1610,18 @@ FAIL: return YES; } +/* When the window is about to close, stop its animation. + Without this, timers might fire after the window is dead. + */ +- (void)windowWillClose:(NSNotification *)notification +{ + NSWindow *win = [notification object]; + NSView *cv = win ? [win contentView] : 0; + ScreenSaverView *sv = cv ? find_saverView (cv) : 0; + if (sv && [sv isAnimating]) + [sv stopAnimation]; +} + # else // USE_IPHONE - (void)applicationWillResignActive:(UIApplication *)app