1/* xscreensaver, Copyright (c) 2006-2019 Jamie Zawinski <jwz@jwz.org> 2 * 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 9 * implied warranty. 10 */ 11 12/* This program serves three purposes: 13 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. 19 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. 25 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. 30 */ 31 32#import <TargetConditionals.h> 33#import "SaverRunner.h" 34#import "SaverListController.h" 35#import "XScreenSaverGLView.h" 36#import "yarandom.h" 37 38#ifdef USE_IPHONE 39 40# ifndef __IPHONE_8_0 41# define UIInterfaceOrientationUnknown UIDeviceOrientationUnknown 42# endif 43# ifndef NSFoundationVersionNumber_iOS_7_1 44# define NSFoundationVersionNumber_iOS_7_1 1047.25 45# endif 46# ifndef NSFoundationVersionNumber_iOS_8_0 47# define NSFoundationVersionNumber_iOS_8_0 1134.10 48# endif 49 50@interface RotateyViewController : UINavigationController 51{ 52 BOOL allowRotation; 53} 54@end 55 56@implementation RotateyViewController 57 58/* This subclass exists so that we can ask that the SaverListController and 59 preferences panels be auto-rotated by the system. Note that the 60 XScreenSaverView is not auto-rotated because it is on a different UIWindow. 61 */ 62 63- (id)initWithRotation:(BOOL)rotatep 64{ 65 self = [super init]; 66 allowRotation = rotatep; 67 return self; 68} 69 70#pragma clang diagnostic push 71#pragma clang diagnostic ignored "-Wdeprecated-implementations" 72- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o 73{ 74 return allowRotation; /* Deprecated in iOS 6 */ 75} 76#pragma clang diagnostic pop 77 78- (BOOL)shouldAutorotate /* Added in iOS 6 */ 79{ 80 return allowRotation; 81} 82 83- (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */ 84{ 85 return UIInterfaceOrientationMaskAll; 86} 87 88@end 89 90 91@implementation SaverViewController 92 93@synthesize saverName; 94 95- (id)initWithSaverRunner:(SaverRunner *)parent 96 showAboutBox:(BOOL)showAboutBox 97{ 98 self = [super init]; 99 if (self) { 100 _parent = parent; 101 _showAboutBox = showAboutBox; 102 103 self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; 104 105# ifndef __IPHONE_7_0 106 self.wantsFullScreenLayout = YES; // Deprecated as of iOS 7 107# endif 108 } 109 return self; 110} 111 112- (BOOL) prefersStatusBarHidden 113{ 114 // Requires UIViewControllerBasedStatusBarAppearance = true in plist 115 return YES; 116} 117 118- (void)dealloc 119{ 120 [saverName release]; 121 // iOS: When a UIView deallocs, it doesn't do [UIView removeFromSuperView] 122 // for its subviews, so the subviews end up with a dangling pointer in their 123 // superview properties. 124 [aboutBox removeFromSuperview]; 125 [aboutBox release]; 126 [_saverView removeFromSuperview]; 127 [_saverView release]; 128 [super dealloc]; 129} 130 131 132- (void)loadView 133{ 134 // The UIViewController's view must never change, so it gets set here to 135 // a plain black background. 136 137 // This background view doesn't block the status bar, but that's probably 138 // OK, because it's never on screen for more than a fraction of a second. 139 UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull]; 140 backgroundView.backgroundColor = [UIColor blackColor]; 141 self.view = backgroundView; 142 [backgroundView release]; 143} 144 145 146- (void)aboutPanel:(UIView *)saverView 147 orientation:(UIInterfaceOrientation)orient 148{ 149 if (!_showAboutBox) 150 return; 151 152 NSString *name = _saverName; 153 NSString *year = [_parent makeDesc:_saverName yearOnly:YES]; 154 155 156 CGRect frame = [saverView frame]; 157 CGFloat rot; 158 CGFloat pt1 = 24; 159 CGFloat pt2 = 14; 160 UIFont *font1 = [UIFont boldSystemFontOfSize: pt1]; 161 UIFont *font2 = [UIFont italicSystemFontOfSize:pt2]; 162 163# ifdef __IPHONE_7_0 164 CGSize s = CGSizeMake(frame.size.width, frame.size.height); 165 CGSize tsize1 = [[[NSAttributedString alloc] 166 initWithString: name 167 attributes:@{ NSFontAttributeName: font1 }] 168 boundingRectWithSize: s 169 options: NSStringDrawingUsesLineFragmentOrigin 170 context: nil].size; 171 CGSize tsize2 = [[[NSAttributedString alloc] 172 initWithString: name 173 attributes:@{ NSFontAttributeName: font2 }] 174 boundingRectWithSize: s 175 options: NSStringDrawingUsesLineFragmentOrigin 176 context: nil].size; 177# else // iOS 6 or Cocoa 178 CGSize tsize1 = [name sizeWithFont:font1 179 constrainedToSize:CGSizeMake(frame.size.width, 180 frame.size.height)]; 181 CGSize tsize2 = [year sizeWithFont:font2 182 constrainedToSize:CGSizeMake(frame.size.width, 183 frame.size.height)]; 184# endif 185 186 CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ? 187 tsize1.width : tsize2.width, 188 tsize1.height + tsize2.height); 189 190 tsize.width = ceilf(tsize.width); 191 tsize.height = ceilf(tsize.height); 192 193 // Don't know how to find inner margin of UITextView. 194 CGFloat margin = 10; 195 tsize.width += margin * 4; 196 tsize.height += margin * 2; 197 198 if ([saverView frame].size.width >= 768) 199 tsize.height += pt1 * 3; // extra bottom margin on iPad 200 201 frame = CGRectMake (0, 0, tsize.width, tsize.height); 202 203 /* Get the text oriented properly, and move it to the bottom of the 204 screen, since many savers have action in the middle. 205 */ 206 switch (orient) { 207 case UIInterfaceOrientationLandscapeLeft: 208 rot = -M_PI/2; 209 frame.origin.x = ([saverView frame].size.width 210 - (tsize.width - tsize.height) / 2 211 - tsize.height); 212 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2; 213 break; 214 case UIInterfaceOrientationLandscapeRight: 215 rot = M_PI/2; 216 frame.origin.x = -(tsize.width - tsize.height) / 2; 217 frame.origin.y = ([saverView frame].size.height - tsize.height) / 2; 218 break; 219 case UIInterfaceOrientationPortraitUpsideDown: 220 rot = M_PI; 221 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2; 222 frame.origin.y = 0; 223 break; 224 default: 225 rot = 0; 226 frame.origin.x = ([saverView frame].size.width - tsize.width) / 2; 227 frame.origin.y = [saverView frame].size.height - tsize.height; 228 break; 229 } 230 231 if (aboutBox) { 232 [aboutBox removeFromSuperview]; 233 [aboutBox release]; 234 } 235 236 aboutBox = [[UIView alloc] initWithFrame:frame]; 237 238 aboutBox.transform = CGAffineTransformMakeRotation (rot); 239 aboutBox.backgroundColor = [UIColor clearColor]; 240 241 /* There seems to be no easy way to stroke the font, so instead draw 242 it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add 243 a black shadow to each. (You'd think the shadow alone would be 244 enough, but there's no way to make it dark enough to be legible.) 245 */ 246 for (int i = 0; i < 5; i++) { 247 UITextView *textview; 248 int off = 1; 249 frame.origin.x = frame.origin.y = 0; 250 switch (i) { 251 case 0: frame.origin.x = -off; break; 252 case 1: frame.origin.x = off; break; 253 case 2: frame.origin.y = -off; break; 254 case 3: frame.origin.y = off; break; 255 } 256 257 for (int j = 0; j < 2; j++) { 258 259 frame.origin.y = (j == 0 ? 0 : pt1); 260 textview = [[UITextView alloc] initWithFrame:frame]; 261 textview.font = (j == 0 ? font1 : font2); 262 textview.text = (j == 0 ? name : year); 263 textview.textAlignment = NSTextAlignmentCenter; 264 textview.showsHorizontalScrollIndicator = NO; 265 textview.showsVerticalScrollIndicator = NO; 266 textview.scrollEnabled = NO; 267 textview.editable = NO; 268 textview.userInteractionEnabled = NO; 269 textview.backgroundColor = [UIColor clearColor]; 270 textview.textColor = (i == 4 271 ? [UIColor yellowColor] 272 : [UIColor blackColor]); 273 274 CALayer *textLayer = (CALayer *) 275 [textview.layer.sublayers objectAtIndex:0]; 276 textLayer.shadowColor = [UIColor blackColor].CGColor; 277 textLayer.shadowOffset = CGSizeMake(0, 0); 278 textLayer.shadowOpacity = 1; 279 textLayer.shadowRadius = 2; 280 281 [aboutBox addSubview:textview]; 282 } 283 } 284 285 CABasicAnimation *anim = 286 [CABasicAnimation animationWithKeyPath:@"opacity"]; 287 anim.duration = 0.3; 288 anim.repeatCount = 1; 289 anim.autoreverses = NO; 290 anim.fromValue = [NSNumber numberWithFloat:0.0]; 291 anim.toValue = [NSNumber numberWithFloat:1.0]; 292 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"]; 293 294 [saverView addSubview:aboutBox]; 295 296 if (splashTimer) 297 [splashTimer invalidate]; 298 299 splashTimer = 300 [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2 301 target:self 302 selector:@selector(aboutOff) 303 userInfo:nil 304 repeats:NO]; 305} 306 307 308- (void)aboutOff 309{ 310 [self aboutOff:FALSE]; 311} 312 313- (void)aboutOff:(BOOL)fast 314{ 315 if (aboutBox) { 316 if (splashTimer) { 317 [splashTimer invalidate]; 318 splashTimer = 0; 319 } 320 if (fast) { 321 aboutBox.layer.opacity = 0; 322 return; 323 } 324 325 CABasicAnimation *anim = 326 [CABasicAnimation animationWithKeyPath:@"opacity"]; 327 anim.duration = 0.3; 328 anim.repeatCount = 1; 329 anim.autoreverses = NO; 330 anim.fromValue = [NSNumber numberWithFloat: 1]; 331 anim.toValue = [NSNumber numberWithFloat: 0]; 332 // anim.delegate = self; 333 aboutBox.layer.opacity = 0; 334 [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"]; 335 } 336} 337 338 339- (void)createSaverView 340{ 341 UIView *parentView = self.view; 342 343 if (_saverView) { 344 [_saverView removeFromSuperview]; 345 [_saverView release]; 346 } 347 348 _saverView = [_parent newSaverView:_saverName 349 withSize:parentView.bounds.size]; 350 351 if (! _saverView) { 352 UIAlertController *c = [UIAlertController 353 alertControllerWithTitle: 354 NSLocalizedString(@"Unable to load!", @"") 355 message:@"" 356 preferredStyle:UIAlertControllerStyleAlert]; 357 [c addAction: [UIAlertAction actionWithTitle: 358 NSLocalizedString(@"Bummer", @"") 359 style: UIAlertActionStyleDefault 360 handler: ^(UIAlertAction *a) { 361 // #### Should expose the SaverListController... 362 }]]; 363 [self presentViewController:c animated:YES completion:nil]; 364 365 return; 366 } 367 368 _saverView.delegate = _parent; 369 _saverView.autoresizingMask = 370 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 371 372 [self.view addSubview:_saverView]; 373 374 // The first responder must be set only after the view was placed in the view 375 // heirarchy. 376 [_saverView becomeFirstResponder]; // For shakes on iOS 6. 377 [_saverView startAnimation]; 378 [self aboutPanel:_saverView 379 orientation: UIInterfaceOrientationPortrait]; 380} 381 382 383- (void)viewDidAppear:(BOOL)animated 384{ 385 [super viewDidAppear:animated]; 386 [self createSaverView]; 387} 388 389 390#pragma clang diagnostic push 391#pragma clang diagnostic ignored "-Wdeprecated-implementations" 392- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o 393{ 394 return NO; /* Deprecated in iOS 6 */ 395} 396#pragma clang diagnostic pop 397 398 399- (BOOL)shouldAutorotate /* Added in iOS 6 */ 400{ 401 return 402 NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 ? 403 ![_saverView suppressRotationAnimation] : 404 YES; 405} 406 407 408- (UIInterfaceOrientationMask)supportedInterfaceOrientations /* Added in iOS 6 */ 409{ 410 // Lies from the iOS docs: 411 // "This method is only called if the view controller's shouldAutorotate 412 // method returns YES." 413 return UIInterfaceOrientationMaskAll; 414} 415 416 417/* 418- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation 419{ 420 return UIInterfaceOrientationPortrait; 421} 422*/ 423 424 425- (void)setSaverName:(NSString *)name 426{ 427 [name retain]; 428 [_saverName release]; 429 _saverName = name; 430 if (_saverView) 431 [self createSaverView]; 432} 433 434 435- (void)viewWillTransitionToSize: (CGSize)size 436 withTransitionCoordinator: 437 (id<UIViewControllerTransitionCoordinator>) coordinator 438{ 439 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; 440 441 if (!_saverView) 442 return; 443 444 [CATransaction begin]; 445 446 // Completely suppress the rotation animation, since we 447 // will not (visually) be rotating at all. 448 if ([_saverView suppressRotationAnimation]) 449 [CATransaction setDisableActions:YES]; 450 451 [self aboutOff:TRUE]; // It does goofy things if we rotate while it's up 452 453# if 1 454 NSLog(@"## orient"); 455 [CATransaction commit]; 456 [_saverView orientationChanged]; 457 return; 458# endif 459 460 BOOL queued = 461 [coordinator animateAlongsideTransition:^ 462 (id <UIViewControllerTransitionCoordinatorContext> context) { 463 // This executes repeatedly during the rotation. 464NSLog(@"## animate %@", context); 465 } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) { 466NSLog(@"## completion %@", context); 467 // This executes once when the rotation has finished. 468 [CATransaction commit]; 469 [_saverView orientationChanged]; 470 }]; 471 // No code goes here, as it would execute before the above completes. 472 473 NSLog(@"## queued = %d", queued); 474 475} 476 477/* Not called 478- (void)willTransitionToTraitCollection:(UITraitCollection *)collection 479 withTransitionCoordinator: 480 (id<UIViewControllerTransitionCoordinator>)coordinator 481{ 482 NSLog(@"#### %@ %@", collection, coordinator); 483} 484*/ 485 486@end 487 488#endif // USE_IPHONE 489 490 491@implementation SaverRunner 492 493 494- (XScreenSaverView *) newSaverView: (NSString *) module 495 withSize: (NSSize) size 496{ 497 Class new_class = 0; 498 499# ifndef USE_IPHONE 500 501 // Load the XScreenSaverView subclass and code from a ".saver" bundle. 502 503 NSString *name = [module stringByAppendingPathExtension:@"saver"]; 504 NSString *path = [saverDir stringByAppendingPathComponent:name]; 505 506 if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) { 507 NSLog(@"bundle \"%@\" does not exist", path); 508 return 0; 509 } 510 511 NSLog(@"Loading %@", path); 512 513 // NSBundle *obundle = saverBundle; 514 515 saverBundle = [NSBundle bundleWithPath:path]; 516 if (saverBundle) 517 new_class = [saverBundle principalClass]; 518 519 // Not entirely unsurprisingly, this tends to break the world. 520 // if (obundle && obundle != saverBundle) 521 // [obundle unload]; 522 523# else // USE_IPHONE 524 525 // Determine whether to create an X11 view or an OpenGL view by 526 // looking for the "gl" tag in the xml file. This is kind of awful. 527 528 NSString *path = [saverDir 529 stringByAppendingPathComponent: 530 [[[module lowercaseString] 531 stringByReplacingOccurrencesOfString:@" " 532 withString:@""] 533 stringByAppendingPathExtension:@"xml"]]; 534 NSData *xmld = [NSData dataWithContentsOfFile:path]; 535 NSAssert (xmld, @"no XML: %@", path); 536 NSString *xml = [XScreenSaverView decompressXML:xmld]; 537 Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0); 538 539 new_class = (gl_p 540 ? [XScreenSaverGLView class] 541 : [XScreenSaverView class]); 542 543# endif // USE_IPHONE 544 545 if (! new_class) 546 return 0; 547 548 NSRect rect; 549 rect.origin.x = rect.origin.y = 0; 550 rect.size.width = size.width; 551 rect.size.height = size.height; 552 553 XScreenSaverView *instance = 554 [(XScreenSaverView *) [new_class alloc] 555 initWithFrame:rect 556 saverName:module 557 isPreview:YES]; 558 if (! instance) { 559 NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module); 560 return 0; 561 } 562 563 564 /* KLUGE: Inform the underlying program that we're in "standalone" 565 mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver". 566 This is kind of horrible but I haven't thought of a more sensible 567 way to make this work. 568 */ 569# ifndef USE_IPHONE 570 if ([saverNames count] == 1) { 571 setenv ("XSCREENSAVER_STANDALONE", "1", 1); 572 } 573# endif 574 575 return (XScreenSaverView *) instance; 576} 577 578 579#ifndef USE_IPHONE 580 581static ScreenSaverView * 582find_saverView_child (NSView *v) 583{ 584 NSArray *kids = [v subviews]; 585 NSUInteger nkids = [kids count]; 586 NSUInteger i; 587 for (i = 0; i < nkids; i++) { 588 NSObject *kid = [kids objectAtIndex:i]; 589 if ([kid isKindOfClass:[ScreenSaverView class]]) { 590 return (ScreenSaverView *) kid; 591 } else { 592 ScreenSaverView *sv = find_saverView_child ((NSView *) kid); 593 if (sv) return sv; 594 } 595 } 596 return 0; 597} 598 599 600static ScreenSaverView * 601find_saverView (NSView *v) 602{ 603 while (1) { 604 NSView *p = [v superview]; 605 if (p) v = p; 606 else break; 607 } 608 return find_saverView_child (v); 609} 610 611 612/* Changes the contents of the menubar menus to correspond to 613 the running saver. Desktop only. 614 */ 615static void 616relabel_menus (NSObject *v, NSString *old_str, NSString *new_str) 617{ 618 if ([v isKindOfClass:[NSMenu class]]) { 619 NSMenu *m = (NSMenu *)v; 620 [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str 621 withString:new_str]]; 622 NSArray *kids = [m itemArray]; 623 NSUInteger nkids = [kids count]; 624 NSUInteger i; 625 for (i = 0; i < nkids; i++) { 626 relabel_menus ([kids objectAtIndex:i], old_str, new_str); 627 } 628 } else if ([v isKindOfClass:[NSMenuItem class]]) { 629 NSMenuItem *mi = (NSMenuItem *)v; 630 [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str 631 withString:new_str]]; 632 NSMenu *m = [mi submenu]; 633 if (m) relabel_menus (m, old_str, new_str); 634 } 635} 636 637 638- (void) openPreferences: (id) sender 639{ 640 ScreenSaverView *sv; 641 if ([sender isKindOfClass:[NSView class]]) { // Sent from button 642 sv = find_saverView ((NSView *) sender); 643 } else { 644 long i; 645 NSWindow *w = 0; 646 for (i = [windows count]-1; i >= 0; i--) { // Sent from menubar 647 w = [windows objectAtIndex:i]; 648 if ([w isKeyWindow]) break; 649 } 650 sv = find_saverView ([w contentView]); 651 } 652 653 NSAssert (sv, @"no saver view"); 654 if (!sv) return; 655 NSWindow *prefs = [sv configureSheet]; 656 657 [NSApp beginSheet:prefs 658 modalForWindow:[sv window] 659 modalDelegate:self 660 didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:) 661 contextInfo:nil]; 662 NSUInteger code = [NSApp runModalForWindow:prefs]; 663 664 /* Restart the animation if the "OK" button was hit, but not if "Cancel". 665 We have to restart *both* animations, because the xlockmore-style 666 ones will blow up if one re-inits but the other doesn't. 667 */ 668 if (code != NSCancelButton) { 669 if ([sv isAnimating]) 670 [sv stopAnimation]; 671 [sv startAnimation]; 672 } 673} 674 675 676- (void) preferencesClosed: (NSWindow *) sheet 677 returnCode: (int) returnCode 678 contextInfo: (void *) contextInfo 679{ 680 [NSApp stopModalWithCode:returnCode]; 681} 682 683#else // USE_IPHONE 684 685 686- (UIImage *) screenshot 687{ 688 return saved_screenshot; 689} 690 691- (void) saveScreenshot 692{ 693 // Most of this is from: 694 // http://developer.apple.com/library/ios/#qa/qa1703/_index.html 695 // The rotation stuff is by me. 696 697 CGSize size = [[UIScreen mainScreen] bounds].size; 698 699 // iOS 7: Needs to be [[window rootViewController] interfaceOrientation]. 700 // iOS 8: Needs to be UIInterfaceOrientationPortrait. 701 // (interfaceOrientation deprecated in iOS 8) 702 703 UIInterfaceOrientation orient = UIInterfaceOrientationPortrait; 704 /* iOS 8 broke -[UIScreen bounds]. */ 705 706 if (orient == UIInterfaceOrientationLandscapeLeft || 707 orient == UIInterfaceOrientationLandscapeRight) { 708 // Rotate the shape of the canvas 90 degrees. 709 double s = size.width; 710 size.width = size.height; 711 size.height = s; 712 } 713 714 715 // Create a graphics context with the target size 716 // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to 717 // take the scale into consideration 718 // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext 719 720 UIGraphicsBeginImageContextWithOptions (size, NO, 0); 721 722 CGContextRef ctx = UIGraphicsGetCurrentContext(); 723 724 725 // Rotate the graphics context to match current hardware rotation. 726 // 727 switch (orient) { 728 case UIInterfaceOrientationPortraitUpsideDown: 729 CGContextTranslateCTM (ctx, [window center].x, [window center].y); 730 CGContextRotateCTM (ctx, M_PI); 731 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y); 732 break; 733 case UIInterfaceOrientationLandscapeLeft: 734 case UIInterfaceOrientationLandscapeRight: 735 CGContextTranslateCTM (ctx, 736 ([window frame].size.height - 737 [window frame].size.width) / 2, 738 ([window frame].size.width - 739 [window frame].size.height) / 2); 740 CGContextTranslateCTM (ctx, [window center].x, [window center].y); 741 CGContextRotateCTM (ctx, 742 (orient == UIInterfaceOrientationLandscapeLeft 743 ? M_PI/2 744 : -M_PI/2)); 745 CGContextTranslateCTM (ctx, -[window center].x, -[window center].y); 746 break; 747 default: 748 break; 749 } 750 751 // Iterate over every window from back to front 752 // 753 for (UIWindow *win in [[UIApplication sharedApplication] windows]) { 754 if (![win respondsToSelector:@selector(screen)] || 755 [win screen] == [UIScreen mainScreen]) { 756 757 // -renderInContext: renders in the coordinate space of the layer, 758 // so we must first apply the layer's geometry to the graphics context 759 CGContextSaveGState (ctx); 760 761 // Center the context around the window's anchor point 762 CGContextTranslateCTM (ctx, [win center].x, [win center].y); 763 764 // Apply the window's transform about the anchor point 765 CGContextConcatCTM (ctx, [win transform]); 766 767 // Offset by the portion of the bounds left of and above anchor point 768 CGContextTranslateCTM (ctx, 769 -[win bounds].size.width * [[win layer] anchorPoint].x, 770 -[win bounds].size.height * [[win layer] anchorPoint].y); 771 772 // Render the layer hierarchy to the current context 773 [[win layer] renderInContext:ctx]; 774 775 // Restore the context 776 CGContextRestoreGState (ctx); 777 } 778 } 779 780 if (saved_screenshot) 781 [saved_screenshot release]; 782 saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain]; 783 784 UIGraphicsEndImageContext(); 785} 786 787 788- (void) openPreferences: (NSString *) saver 789{ 790 XScreenSaverView *saverView = [self newSaverView:saver 791 withSize:CGSizeMake(0, 0)]; 792 if (! saverView) return; 793 794 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; 795 [prefs setObject:saver forKey:@"selectedSaverName"]; 796 [prefs synchronize]; 797 798 [rotating_nav pushViewController: [saverView configureView] 799 animated:YES]; 800} 801 802 803#endif // USE_IPHONE 804 805 806 807- (void)loadSaver:(NSString *)name 808{ 809# ifndef USE_IPHONE 810 811 if (saverName && [saverName isEqualToString: name]) { 812 for (NSWindow *win in windows) { 813 ScreenSaverView *sv = find_saverView ([win contentView]); 814 if (![sv isAnimating]) 815 [sv startAnimation]; 816 } 817 return; 818 } 819 820 saverName = name; 821 822 for (NSWindow *win in windows) { 823 NSView *cv = [win contentView]; 824 NSString *old_title = [win title]; 825 if (!old_title) old_title = @"XScreenSaver"; 826 [win setTitle: name]; 827 relabel_menus (menubar, old_title, name); 828 829 ScreenSaverView *old_view = find_saverView (cv); 830 NSView *sup = old_view ? [old_view superview] : cv; 831 832 if (old_view) { 833 if ([old_view isAnimating]) 834 [old_view stopAnimation]; 835 [old_view removeFromSuperview]; 836 } 837 838 NSSize size = [cv frame].size; 839 ScreenSaverView *new_view = [self newSaverView:name withSize: size]; 840 NSAssert (new_view, @"unable to make a saver view"); 841 842 [new_view setFrame: (old_view ? [old_view frame] : [cv frame])]; 843 [sup addSubview: new_view]; 844 [win makeFirstResponder:new_view]; 845 [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; 846 [new_view startAnimation]; 847 [new_view release]; 848 } 849 850 NSUserDefaultsController *ctl = 851 [NSUserDefaultsController sharedUserDefaultsController]; 852 [ctl save:self]; 853 854# else // USE_IPHONE 855 856# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR 857 NSLog (@"selecting saver \"%@\"", name); 858# endif 859 860 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; 861 [prefs setObject:name forKey:@"selectedSaverName"]; 862 [prefs synchronize]; 863 864/* Cacheing this screws up rotation when starting a saver twice in a row. 865 if (saverName && [saverName isEqualToString: name]) { 866 if ([saverView isAnimating]) 867 return; 868 else 869 goto LAUNCH; 870 } 871*/ 872 873 saverName = name; 874 875 if (nonrotating_controller) { 876 nonrotating_controller.saverName = name; 877 return; 878 } 879 880# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR 881 UIScreen *screen = [UIScreen mainScreen]; 882 883 /* 'nativeScale' is very confusing. 884 885 iPhone 4s: 886 bounds: 320x480 scale: 2 887 nativeBounds: 640x960 nativeScale: 2 888 iPhone 5s: 889 bounds: 320x568 scale: 2 890 nativeBounds: 640x1136 nativeScale: 2 891 iPad 2: 892 bounds: 768x1024 scale: 1 893 nativeBounds: 768x1024 nativeScale: 1 894 iPad Retina/Air: 895 bounds: 768x1024 scale: 2 896 nativeBounds: 1536x2048 nativeScale: 2 897 iPhone 6: 898 bounds: 320x568 scale: 2 899 nativeBounds: 640x1136 nativeScale: 2 900 iPhone 6+: 901 bounds: 320x568 scale: 2 902 nativeBounds: 960x1704 nativeScale: 3 903 904 According to a StackOverflow comment: 905 906 The iPhone 6+ renders internally using @3x assets at a virtual 907 resolution of 2208x1242 (with 736x414 points), then samples that down 908 for display. The same as using a scaled resolution on a Retina MacBook 909 -- it lets them hit an integral multiple for pixel assets while still 910 having e.g. 12pt text look the same size on the screen. 911 912 The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch, 913 and use @2x assets to stick to the approximately 160 points per inch 914 of all previous devices. 915 916 The 6+ is 401 pixels per inch. So it'd hypothetically need roughly 917 @2.46x assets. Instead Apple uses @3x assets and scales the complete 918 output down to about 84% of its natural size. 919 920 In practice Apple has decided to go with more like 87%, turning the 921 1080 into 1242. No doubt that was to find something as close as 922 possible to 84% that still produced integral sizes in both directions 923 -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080 924 into, say, 1286, you'd somehow need to render 2286.22 pixels 925 vertically to scale well. 926 */ 927 928 NSLog(@"screen: %.0fx%0.f", 929 [[screen currentMode] size].width, 930 [[screen currentMode] size].height); 931 NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f", 932 [screen bounds].size.width, 933 [screen bounds].size.height, 934 [screen scale], 935 [screen scale] * [screen bounds].size.width, 936 [screen scale] * [screen bounds].size.height); 937 938# ifdef __IPHONE_8_0 939 if ([screen respondsToSelector:@selector(nativeBounds)]) 940 NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f", 941 [screen nativeBounds].size.width, 942 [screen nativeBounds].size.height, 943 [screen nativeScale], 944 [screen nativeBounds].size.width / [screen nativeScale], 945 [screen nativeBounds].size.height / [screen nativeScale]); 946# endif 947# endif // TARGET_IPHONE_SIMULATOR 948 949 // Take the screen shot before creating the screen saver view, because this 950 // can screw with the layout. 951 [self saveScreenshot]; 952 953 // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal 954 // presentation full screen. 955 rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen; 956 957 nonrotating_controller = [[SaverViewController alloc] 958 initWithSaverRunner:self 959 showAboutBox:[saverNames count] != 1]; 960 nonrotating_controller.saverName = name; 961 962 // Necessary to prevent "card"-like presentation on Xcode 11 with iOS 13: 963 nonrotating_controller.modalPresentationStyle = 964 UIModalPresentationFullScreen; 965 966 /* LAUNCH: */ 967 968 [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil]; 969 970 // Doing this makes savers cut back to the list instead of fading, 971 // even though [XScreenSaverView stopAndClose] does setHidden:NO first. 972 // [window setHidden:YES]; 973 974# endif // USE_IPHONE 975} 976 977 978#ifndef USE_IPHONE 979 980- (void)aboutPanel:(id)sender 981{ 982 NSDictionary *bd = [saverBundle infoDictionary]; 983 NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20]; 984 985 [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"]; 986 [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"]; 987 [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 988 forKey:@"ApplicationVersion"]; 989 [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"]; 990 NSAttributedString *s = [[NSAttributedString alloc] 991 initWithString: (NSString *) 992 [bd objectForKey:@"CFBundleGetInfoString"]]; 993 [d setValue:s forKey:@"Credits"]; 994 [s release]; 995 996 [[NSApplication sharedApplication] 997 orderFrontStandardAboutPanelWithOptions:d]; 998} 999 1000#endif // !USE_IPHONE 1001 1002 1003 1004- (void)selectedSaverDidChange:(NSDictionary *)change 1005{ 1006 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; 1007 NSString *name = [prefs stringForKey:@"selectedSaverName"]; 1008 1009 if (! name) return; 1010 1011 if (! [saverNames containsObject:name]) { 1012 NSLog (@"saver \"%@\" does not exist", name); 1013 return; 1014 } 1015 1016 [self loadSaver: name]; 1017} 1018 1019 1020- (NSArray *) listSaverBundleNamesInDir:(NSString *)dir 1021{ 1022# ifndef USE_IPHONE 1023 NSString *ext = @"saver"; 1024# else 1025 NSString *ext = @"xml"; 1026# endif 1027 1028 NSArray *files = [[NSFileManager defaultManager] 1029 contentsOfDirectoryAtPath:dir error:nil]; 1030 if (! files) return 0; 1031 NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1]; 1032 1033 for (NSString *p in files) { 1034 if ([[p pathExtension] caseInsensitiveCompare: ext]) 1035 continue; 1036 1037 NSString *name = [[p lastPathComponent] stringByDeletingPathExtension]; 1038 1039# ifdef USE_IPHONE 1040 // Get the saver name's capitalization right by reading the XML file. 1041 1042 p = [dir stringByAppendingPathComponent: p]; 1043 NSData *xmld = [NSData dataWithContentsOfFile:p]; 1044 NSAssert (xmld, @"no XML: %@", p); 1045 NSString *xml = [XScreenSaverView decompressXML:xmld]; 1046 NSRange r = [xml rangeOfString:@"_label=\"" options:0]; 1047 NSAssert1 (r.length, @"no name in %@", p); 1048 if (r.length) { 1049 xml = [xml substringFromIndex: r.location + r.length]; 1050 r = [xml rangeOfString:@"\"" options:0]; 1051 if (r.length) name = [xml substringToIndex: r.location]; 1052 } 1053 1054# endif // USE_IPHONE 1055 1056 NSAssert1 (name, @"no name in %@", p); 1057 if (name) [result addObject: name]; 1058 } 1059 1060 if (result && [result count]) 1061 return [result sortedArrayUsingSelector: 1062 @selector(localizedCaseInsensitiveCompare:)]; 1063 else 1064 return 0; 1065} 1066 1067 1068 1069- (NSArray *) listSaverBundleNames 1070{ 1071 NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10]; 1072 1073# ifndef USE_IPHONE 1074 // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/" 1075 // directories in the bundle. 1076 [dirs addObject: [[[[NSBundle mainBundle] bundlePath] 1077 stringByAppendingPathComponent:@"Contents"] 1078 stringByAppendingPathComponent:@"Resources"]]; 1079 [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]]; 1080 1081 // Also look in the same directory as the executable. 1082 [dirs addObject: [[[NSBundle mainBundle] bundlePath] 1083 stringByDeletingLastPathComponent]]; 1084 1085 // Finally, look in standard MacOS screensaver directories. 1086// [dirs addObject: @"~/Library/Screen Savers"]; 1087// [dirs addObject: @"/Library/Screen Savers"]; 1088// [dirs addObject: @"/System/Library/Screen Savers"]; 1089 1090# else // USE_IPHONE 1091 1092 // On iOS, only look in the bundle's root directory. 1093 [dirs addObject: [[NSBundle mainBundle] bundlePath]]; 1094 1095# endif // USE_IPHONE 1096 1097 int i; 1098 for (i = 0; i < [dirs count]; i++) { 1099 NSString *dir = [dirs objectAtIndex:i]; 1100 NSArray *names = [self listSaverBundleNamesInDir:dir]; 1101 if (! names) continue; 1102 saverDir = [dir retain]; 1103 saverNames = [names retain]; 1104 return names; 1105 } 1106 1107 NSString *err = @"no .saver bundles found in: "; 1108 for (i = 0; i < [dirs count]; i++) { 1109 if (i) err = [err stringByAppendingString:@", "]; 1110 err = [err stringByAppendingString:[[dirs objectAtIndex:i] 1111 stringByAbbreviatingWithTildeInPath]]; 1112 err = [err stringByAppendingString:@"/"]; 1113 } 1114 NSLog (@"%@", err); 1115 return [NSArray array]; 1116} 1117 1118 1119/* Create the popup menu of available saver names. 1120 */ 1121#ifndef USE_IPHONE 1122 1123- (NSPopUpButton *) makeMenu 1124{ 1125 NSRect rect; 1126 rect.origin.x = rect.origin.y = 0; 1127 rect.size.width = 10; 1128 rect.size.height = 10; 1129 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect 1130 pullsDown:NO]; 1131 int i; 1132 float max_width = 0; 1133 for (i = 0; i < [saverNames count]; i++) { 1134 NSString *name = [saverNames objectAtIndex:i]; 1135 [popup addItemWithTitle:name]; 1136 [[popup itemWithTitle:name] setRepresentedObject:name]; 1137 [popup sizeToFit]; 1138 NSRect r = [popup frame]; 1139 if (r.size.width > max_width) max_width = r.size.width; 1140 } 1141 1142 // Bind the menu to preferences, and trigger a callback when an item 1143 // is selected. 1144 // 1145 NSString *key = @"values.selectedSaverName"; 1146 NSUserDefaultsController *prefs = 1147 [NSUserDefaultsController sharedUserDefaultsController]; 1148 [prefs addObserver:self 1149 forKeyPath:key 1150 options:0 1151 context:@selector(selectedSaverDidChange:)]; 1152 [popup bind:@"selectedObject" 1153 toObject:prefs 1154 withKeyPath:key 1155 options:nil]; 1156 [prefs setAppliesImmediately:YES]; 1157 1158 NSRect r = [popup frame]; 1159 r.size.width = max_width; 1160 [popup setFrame:r]; 1161 [popup autorelease]; 1162 return popup; 1163} 1164 1165#else // USE_IPHONE 1166 1167- (NSString *) makeDesc:(NSString *)saver 1168 yearOnly:(BOOL) yearp 1169{ 1170 NSString *desc = 0; 1171 NSString *path = [saverDir stringByAppendingPathComponent: 1172 [[saver lowercaseString] 1173 stringByReplacingOccurrencesOfString:@" " 1174 withString:@""]]; 1175 NSRange r; 1176 1177 path = [path stringByAppendingPathExtension:@"xml"]; 1178 NSData *xmld = [NSData dataWithContentsOfFile:path]; 1179 if (! xmld) goto FAIL; 1180 desc = [XScreenSaverView decompressXML:xmld]; 1181 if (! desc) goto FAIL; 1182 1183 r = [desc rangeOfString:@"<_description>" 1184 options:NSCaseInsensitiveSearch]; 1185 if (r.length == 0) { 1186 desc = 0; 1187 goto FAIL; 1188 } 1189 desc = [desc substringFromIndex: r.location + r.length]; 1190 r = [desc rangeOfString:@"</_description>" 1191 options:NSCaseInsensitiveSearch]; 1192 if (r.length > 0) 1193 desc = [desc substringToIndex: r.location]; 1194 1195 // Leading and trailing whitespace. 1196 desc = [desc stringByTrimmingCharactersInSet: 1197 [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 1198 1199 // Let's see if we can find a year on the last line. 1200 r = [desc rangeOfString:@"\n" options:NSBackwardsSearch]; 1201 NSString *year = 0; 1202 for (NSString *word in 1203 [[desc substringFromIndex:r.location + r.length] 1204 componentsSeparatedByCharactersInSet: 1205 [NSCharacterSet characterSetWithCharactersInString: 1206 @" \t\n-."]]) { 1207 int n = [word doubleValue]; 1208 if (n > 1970 && n < 2100) 1209 year = word; 1210 } 1211 1212 // Delete everything after the first blank line. 1213 // 1214 r = [desc rangeOfString:@"\n\n" options:0]; 1215 if (r.length > 0) 1216 desc = [desc substringToIndex: r.location]; 1217 1218 // Unwrap lines and compress whitespace. 1219 { 1220 NSString *result = @""; 1221 for (NSString *s in [desc componentsSeparatedByCharactersInSet: 1222 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) { 1223 if ([result length] == 0) 1224 result = s; 1225 else if ([s length] > 0) 1226 result = [NSString stringWithFormat: @"%@ %@", result, s]; 1227 desc = result; 1228 } 1229 } 1230 1231 if (year) 1232 desc = [year stringByAppendingString: 1233 [@": " stringByAppendingString: desc]]; 1234 1235 if (yearp) 1236 desc = year ? year : @""; 1237 1238FAIL: 1239 if (! desc) { 1240 if ([saverNames count] > 1) 1241 desc = @"Oops, this module appears to be incomplete."; 1242 else 1243 desc = @""; 1244 } 1245 1246 return desc; 1247} 1248 1249- (NSString *) makeDesc:(NSString *)saver 1250{ 1251 return [self makeDesc:saver yearOnly:NO]; 1252} 1253 1254 1255 1256/* Create a dictionary of one-line descriptions of every saver, 1257 for display on the UITableView. 1258 */ 1259- (NSDictionary *)makeDescTable 1260{ 1261 NSMutableDictionary *dict = 1262 [NSMutableDictionary dictionaryWithCapacity:[saverNames count]]; 1263 for (NSString *saver in saverNames) { 1264 [dict setObject:[self makeDesc:saver] forKey:saver]; 1265 } 1266 return dict; 1267} 1268 1269 1270- (void) wantsFadeOut:(XScreenSaverView *)sender 1271{ 1272 rotating_nav.view.hidden = NO; // In case it was hidden during startup. 1273 1274 /* Make sure the most-recently-run saver is visible. Sometimes it ends 1275 up scrolled half a line off the bottom of the screen. 1276 */ 1277 if (saverName) { 1278 for (UIViewController *v in [rotating_nav viewControllers]) { 1279 if ([v isKindOfClass:[SaverListController class]]) { 1280 [(SaverListController *)v scrollTo: saverName]; 1281 break; 1282 } 1283 } 1284 } 1285 1286 [rotating_nav dismissViewControllerAnimated:YES completion:^() { 1287 [nonrotating_controller release]; 1288 nonrotating_controller = nil; 1289 [[rotating_nav view] becomeFirstResponder]; 1290 }]; 1291} 1292 1293 1294- (void) didShake:(XScreenSaverView *)sender 1295{ 1296# if TARGET_IPHONE_SIMULATOR 1297 NSLog (@"simulating shake on saver list"); 1298# endif 1299 [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake 1300 withEvent: nil]; 1301} 1302 1303 1304#endif // USE_IPHONE 1305 1306 1307 1308/* This is called when the "selectedSaverName" pref changes, e.g., 1309 when a menu selection is made. 1310 */ 1311- (void)observeValueForKeyPath:(NSString *)keyPath 1312 ofObject:(id)object 1313 change:(NSDictionary *)change 1314 context:(void *)context 1315{ 1316 SEL dispatchSelector = (SEL)context; 1317 if (dispatchSelector != NULL) { 1318 [self performSelector:dispatchSelector withObject:change]; 1319 } else { 1320 [super observeValueForKeyPath:keyPath 1321 ofObject:object 1322 change:change 1323 context:context]; 1324 } 1325} 1326 1327 1328# ifndef USE_IPHONE 1329 1330/* Create the desktop window shell, possibly including a preferences button. 1331 */ 1332- (NSWindow *) makeWindow 1333{ 1334 NSRect rect; 1335 static int count = 0; 1336 Bool simple_p = ([saverNames count] == 1); 1337 NSButton *pb = 0; 1338 NSPopUpButton *menu = 0; 1339 NSBox *gbox = 0; 1340 NSBox *pbox = 0; 1341 1342 NSRect sv_rect; 1343 sv_rect.origin.x = sv_rect.origin.y = 0; 1344 sv_rect.size.width = 320; 1345 sv_rect.size.height = 240; 1346 ScreenSaverView *sv = [[ScreenSaverView alloc] // dummy placeholder 1347 initWithFrame:sv_rect 1348 isPreview:YES]; 1349 1350 // make a "Preferences" button 1351 // 1352 if (! simple_p) { 1353 rect.origin.x = 0; 1354 rect.origin.y = 0; 1355 rect.size.width = rect.size.height = 10; 1356 pb = [[NSButton alloc] initWithFrame:rect]; 1357 [pb setTitle:NSLocalizedString(@"Preferences", @"")]; 1358 [pb setBezelStyle:NSRoundedBezelStyle]; 1359 [pb sizeToFit]; 1360 1361 rect.origin.x = ([sv frame].size.width - 1362 [pb frame].size.width) / 2; 1363 [pb setFrameOrigin:rect.origin]; 1364 1365 // grab the click 1366 // 1367 [pb setTarget:self]; 1368 [pb setAction:@selector(openPreferences:)]; 1369 1370 // Make a saver selection menu 1371 // 1372 menu = [self makeMenu]; 1373 rect.origin.x = 2; 1374 rect.origin.y = 2; 1375 [menu setFrameOrigin:rect.origin]; 1376 1377 // make a box to wrap the saverView 1378 // 1379 rect = [sv frame]; 1380 rect.origin.x = 0; 1381 rect.origin.y = [pb frame].origin.y + [pb frame].size.height; 1382 gbox = [[NSBox alloc] initWithFrame:rect]; 1383 rect.size.width = rect.size.height = 10; 1384 [gbox setContentViewMargins:rect.size]; 1385 [gbox setTitlePosition:NSNoTitle]; 1386 [gbox addSubview:sv]; 1387 [gbox sizeToFit]; 1388 1389 // make a box to wrap the other two boxes 1390 // 1391 rect.origin.x = rect.origin.y = 0; 1392 rect.size.width = [gbox frame].size.width; 1393 rect.size.height = [gbox frame].size.height + [gbox frame].origin.y; 1394 pbox = [[NSBox alloc] initWithFrame:rect]; 1395 [pbox setTitlePosition:NSNoTitle]; 1396 [pbox setBorderType:NSNoBorder]; 1397 [pbox addSubview:gbox]; 1398 [gbox release]; 1399 if (menu) [pbox addSubview:menu]; 1400 if (pb) [pbox addSubview:pb]; 1401 [pb release]; 1402 [pbox sizeToFit]; 1403 1404 [pb setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin]; 1405 [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin]; 1406 [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; 1407 [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; 1408 } 1409 1410 [sv setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; 1411 1412 1413 // and make a window to hold that. 1414 // 1415 NSScreen *screen = [NSScreen mainScreen]; 1416 rect = pbox ? [pbox frame] : [sv frame]; 1417 rect.origin.x = ([screen frame].size.width - rect.size.width) / 2; 1418 rect.origin.y = ([screen frame].size.height - rect.size.height) / 2; 1419 1420 rect.origin.x += rect.size.width * (count ? 0.55 : -0.55); 1421 1422 NSWindow *win = [[NSWindow alloc] 1423 initWithContentRect:rect 1424 styleMask:(NSTitledWindowMask | 1425 NSClosableWindowMask | 1426 NSMiniaturizableWindowMask | 1427 NSResizableWindowMask) 1428 backing:NSBackingStoreBuffered 1429 defer:YES 1430 screen:screen]; 1431// [win setMinSize:[win frameRectForContentRect:rect].size]; 1432 [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)]; 1433 [pbox release]; 1434 1435 [win makeKeyAndOrderFront:win]; 1436 1437 [sv startAnimation]; // this is the dummy saver 1438 [sv autorelease]; 1439 1440 count++; 1441 1442 return win; 1443} 1444 1445 1446- (void) animTimer 1447{ 1448 for (NSWindow *win in windows) { 1449 ScreenSaverView *sv = find_saverView ([win contentView]); 1450 if ([sv isAnimating]) 1451 [sv animateOneFrame]; 1452 } 1453} 1454 1455# endif // !USE_IPHONE 1456 1457 1458- (void)applicationDidFinishLaunching: 1459# ifndef USE_IPHONE 1460 (NSNotification *) notif 1461# else // USE_IPHONE 1462 (UIApplication *) application 1463# endif // USE_IPHONE 1464{ 1465 [self listSaverBundleNames]; 1466 1467 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; 1468 1469# ifndef USE_IPHONE 1470 int window_count = ([saverNames count] <= 1 ? 1 : 2); 1471 NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1] 1472 retain]; 1473 windows = a; 1474 1475 int i; 1476 // Create either one window (for standalone, e.g. Phosphor.app) 1477 // or two windows for SaverTester.app. 1478 for (i = 0; i < window_count; i++) { 1479 NSWindow *win = [self makeWindow]; 1480 [win setDelegate:self]; 1481 // Get the last-saved window position out of preferences. 1482 [win setFrameAutosaveName: 1483 [NSString stringWithFormat:@"XScreenSaverWindow%d", i]]; 1484 [win setFrameUsingName:[win frameAutosaveName]]; 1485 [a addObject: win]; 1486 // This prevents clicks from being seen by savers. 1487 // [win setMovableByWindowBackground:YES]; 1488 win.releasedWhenClosed = NO; 1489 [win release]; 1490 } 1491# else // USE_IPHONE 1492 1493# undef ya_rand_init 1494 ya_rand_init (0); // Now's a good time. 1495 1496 1497 /* iOS docs say: 1498 "You must call this method before attempting to get orientation data from 1499 the receiver. This method enables the device's accelerometer hardware 1500 and begins the delivery of acceleration events to the receiver." 1501 1502 Adding or removing this doesn't seem to make any difference. It's 1503 probably getting called by the UINavigationController. Still... */ 1504 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; 1505 1506 rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES] 1507 retain]; 1508 1509 if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup. 1510 rotating_nav.view.hidden = YES; 1511 1512 [window setRootViewController: rotating_nav]; 1513 [window setAutoresizesSubviews:YES]; 1514 [window setAutoresizingMask: 1515 (UIViewAutoresizingFlexibleWidth | 1516 UIViewAutoresizingFlexibleHeight)]; 1517 1518 SaverListController *menu = [[SaverListController alloc] 1519 initWithNames:saverNames 1520 descriptions:[self makeDescTable]]; 1521 [rotating_nav pushViewController:menu animated:YES]; 1522 [menu becomeFirstResponder]; 1523 [menu autorelease]; 1524 1525 application.applicationSupportsShakeToEdit = YES; 1526 1527 1528# endif // USE_IPHONE 1529 1530 NSString *forced = 0; 1531 /* In the XCode project, each .saver scheme sets this env var when 1532 launching SaverTester.app so that it knows which one we are 1533 currently debugging. If this is set, it overrides the default 1534 selection in the popup menu. If unset, that menu persists to 1535 whatever it was last time. 1536 */ 1537 const char *f = getenv ("SELECTED_SAVER"); 1538 if (f && *f) 1539 forced = [NSString stringWithCString:(char *)f 1540 encoding:NSUTF8StringEncoding]; 1541 1542 if (forced && ![saverNames containsObject:forced]) { 1543 NSLog(@"forced saver \"%@\" does not exist", forced); 1544 forced = 0; 1545 } 1546 1547 // If there's only one saver, run that. 1548 if (!forced && [saverNames count] == 1) 1549 forced = [saverNames objectAtIndex:0]; 1550 1551# ifdef USE_IPHONE 1552 NSString *prev = [prefs stringForKey:@"selectedSaverName"]; 1553 1554 if (forced) 1555 prev = forced; 1556 1557 // If nothing was selected (e.g., this is the first launch) 1558 // then scroll randomly instead of starting up at "A". 1559 // 1560 if (!prev) 1561 prev = [saverNames objectAtIndex: (random() % [saverNames count])]; 1562 1563 if (prev) 1564 [menu scrollTo: prev]; 1565# endif // USE_IPHONE 1566 1567 if (forced) 1568 [prefs setObject:forced forKey:@"selectedSaverName"]; 1569 1570# ifdef USE_IPHONE 1571 /* Don't auto-launch the saver unless it was running last time. 1572 XScreenSaverView manages this, on crash_timer. 1573 Unless forced. 1574 */ 1575 if (!forced && ![prefs boolForKey:@"wasRunning"]) 1576 return; 1577# endif 1578 1579 [self selectedSaverDidChange:nil]; 1580// [NSTimer scheduledTimerWithTimeInterval: 0 1581// target:self 1582// selector:@selector(selectedSaverDidChange:) 1583// userInfo:nil 1584// repeats:NO]; 1585 1586 1587 1588# ifndef USE_IPHONE 1589 /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the 1590 ScreenSaverView to run its own timer calling animateOneFrame. 1591 On 10.9, that fails because the private class ScreenSaverModule 1592 is only initialized properly by ScreenSaverEngine, and in the 1593 context of SaverRunner, the null ScreenSaverEngine instance 1594 behaves as if [ScreenSaverEngine needsAnimationTimer] returned false. 1595 So, if it looks like this is the 10.9 version of ScreenSaverModule 1596 instead of the 10.8 version, we run our own timer here. This sucks. 1597 */ 1598 if (!anim_timer) { 1599 Class ssm = NSClassFromString (@"ScreenSaverModule"); 1600 if (ssm && [ssm instancesRespondToSelector: 1601 NSSelectorFromString(@"needsAnimationTimer")]) { 1602 NSWindow *win = [windows objectAtIndex:0]; 1603 ScreenSaverView *sv = find_saverView ([win contentView]); 1604 anim_timer = [NSTimer scheduledTimerWithTimeInterval: 1605 [sv animationTimeInterval] 1606 target:self 1607 selector:@selector(animTimer) 1608 userInfo:nil 1609 repeats:YES]; 1610 } 1611 } 1612# endif // !USE_IPHONE 1613} 1614 1615 1616#ifndef USE_IPHONE 1617 1618/* When the window closes, exit (even if prefs still open.) 1619 */ 1620- (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n 1621{ 1622 return YES; 1623} 1624 1625/* When the window is about to close, stop its animation. 1626 Without this, timers might fire after the window is dead. 1627 */ 1628- (void)windowWillClose:(NSNotification *)notification 1629{ 1630 NSWindow *win = [notification object]; 1631 NSView *cv = win ? [win contentView] : 0; 1632 ScreenSaverView *sv = cv ? find_saverView (cv) : 0; 1633 if (sv && [sv isAnimating]) 1634 [sv stopAnimation]; 1635} 1636 1637# else // USE_IPHONE 1638 1639- (void)applicationWillResignActive:(UIApplication *)app 1640{ 1641 [(XScreenSaverView *)view setScreenLocked:YES]; 1642} 1643 1644- (void)applicationDidBecomeActive:(UIApplication *)app 1645{ 1646 [(XScreenSaverView *)view setScreenLocked:NO]; 1647} 1648 1649- (void)applicationDidEnterBackground:(UIApplication *)application 1650{ 1651 [(XScreenSaverView *)view setScreenLocked:YES]; 1652} 1653 1654#endif // USE_IPHONE 1655 1656 1657@end 1658