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