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