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