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