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