1 /* xscreensaver, Copyright © 2006-2023 Jamie Zawinski <jwz@jwz.org>
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
12 /* This is a subclass of Apple's ScreenSaverView that knows how to run
13 xscreensaver programs without X11 via the dark magic of the "jwxyz"
14 library. In xscreensaver terminology, this is the replacement for
15 the "screenhack.c" module.
18 #import <QuartzCore/QuartzCore.h>
22 # include <execinfo.h>
24 #import "XScreenSaverView.h"
25 #import "XScreenSaverConfigSheet.h"
27 #import "screenhackI.h"
30 #import "jwxyz-cocoa.h"
31 #import "jwxyz-timers.h"
35 // XScreenSaverView.m speaks OpenGL ES just fine, but enableBackbuffer does
36 // need (jwzgles_)gluCheckExtension.
39 # import <OpenGL/glu.h>
43 # define VENTURA_KLUDGE
44 # define SONOMA_KLUDGE
49 #define countof(x) (sizeof((x))/sizeof((*x)))
52 /* Duplicated in xlockmoreI.h and XScreenSaverGLView.m. */
53 extern void clear_gl_error (void);
54 extern void check_gl_error (const char *type);
56 extern struct xscreensaver_function_table *xscreensaver_function_table;
58 /* Global variables used by the screen savers
61 const char *progclass;
66 # define NSSizeToCGSize(x) (x)
68 extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
70 /* Stub definition of the superclass, for iPhone.
72 @implementation ScreenSaverView
74 NSTimeInterval anim_interval;
79 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
80 self = [super initWithFrame:frame];
82 anim_interval = 1.0/30;
85 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
86 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
87 - (BOOL)hasConfigureSheet { return NO; }
88 - (NSWindow *)configureSheet { return nil; }
89 - (NSView *)configureView { return nil; }
90 - (BOOL)isPreview { return NO; }
91 - (BOOL)isAnimating { return animating_p; }
92 - (void)animateOneFrame { }
94 - (void)startAnimation {
95 if (animating_p) return;
97 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
99 selector:@selector(animateOneFrame)
104 - (void)stopAnimation {
106 [anim_timer invalidate];
113 # endif // !HAVE_IPHONE
117 @interface XScreenSaverView (Private)
118 - (void) stopAndClose;
119 - (void) stopAndClose:(Bool)relaunch;
122 @implementation XScreenSaverView
124 // Given a lower-cased saver name, returns the function table for it.
125 // If no name, guess the name from the class's bundle name.
127 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)title
129 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
130 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
132 NSString *path = [nsb bundlePath];
133 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
135 kCFURLPOSIXPathStyle,
137 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
139 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
140 // #### Analyze says "Potential leak of an object stored into cfb"
143 // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
144 // I'm guessing that symbol-stripping is mandatory. Fuck.
145 NSString *classname = [[nsb executablePath] lastPathComponent];
146 NSString *table_name = [[classname lowercaseString]
147 stringByAppendingString:
148 @"_xscreensaver_function_table"];
149 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
153 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
155 # else // HAVE_IPHONE
156 // Depends on the auto-generated "ios-function-table.m" being up to date.
157 if (! function_tables)
158 function_tables = [make_function_table_dict() retain];
159 NSValue *v = [function_tables objectForKey: title];
160 void *addr = v ? [v pointerValue] : 0;
161 # endif // HAVE_IPHONE
163 return (struct xscreensaver_function_table *) addr;
167 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
168 // to $PATH for the benefit of savers that include helper shell scripts.
170 - (void) setShellPath
172 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
173 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
175 NSString *nsrespath = [nsb resourcePath]; // "Contents/Resources"
176 NSString *nsexepath = [[nsb executablePath] // "Contents/MacOS/CLASSNAME"
177 stringByDeletingLastPathComponent];
178 NSAssert1 (nsrespath, @"no resourcePath for class %@", [self class]);
179 NSAssert1 (nsexepath, @"no executablePath for class %@", [self class]);
180 const char *respath = [nsrespath cStringUsingEncoding:NSUTF8StringEncoding];
181 const char *exepath = [nsexepath cStringUsingEncoding:NSUTF8StringEncoding];
183 // Only read $PATH once, so that when running under Randomizer.m,
184 // we don't keep adding more and more to it with each selected saver.
185 static const char *opath = 0;
187 opath = getenv ("PATH");
188 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
189 opath = strdup (opath); // Leaks once, NBD.
192 char *npath = (char *) malloc (strlen (opath) + 2 +
193 strlen (respath) + 2 +
194 strlen (exepath) + 2);
195 strcpy (npath, exepath);
197 strcat (npath, respath);
199 strcat (npath, opath);
200 if (setenv ("PATH", npath, 1)) {
202 NSAssert1 (0, @"setenv \"PATH=%s\" failed", npath);
209 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
210 // (e.g., "xscreensaver-text") know how to look up resources.
212 - (void) setResourcesEnv:(NSString *) name
214 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
215 if (setenv ("XSCREENSAVER_CLASSPATH", s, 1)) {
217 NSAssert1 (0, @"setenv \"XSCREENSAVER_CLASSPATH=%s\" failed", s);
222 - (void) loadCustomFonts
225 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
226 NSMutableArray *fonts = [NSMutableArray arrayWithCapacity:20];
227 for (NSString *ext in @[@"ttf", @"otf"]) {
228 [fonts addObjectsFromArray: [nsb pathsForResourcesOfType:ext
231 for (NSString *font in fonts) {
232 CFURLRef url = (CFURLRef) [NSURL fileURLWithPath: font];
234 if (! CTFontManagerRegisterFontsForURL (url, kCTFontManagerScopeProcess,
236 // Just ignore errors:
237 // "The file has already been registered in the specified scope."
238 // NSLog (@"loading font: %@ %@", url, err);
241 # endif // !HAVE_IPHONE
246 add_default_options (const XrmOptionDescRec *opts,
247 const char * const *defs,
248 XrmOptionDescRec **opts_ret,
249 const char ***defs_ret)
251 /* These aren't "real" command-line options (there are no actual command-line
252 options in the Cocoa version); but this is the somewhat kludgey way that
253 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
254 ../hacks/config/\*.xml files communicate with the preferences database.
256 static const XrmOptionDescRec default_options [] = {
257 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
258 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
259 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
260 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
261 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
262 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
263 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
264 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
265 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
266 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
267 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
268 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
269 { "-foreground", ".foreground", XrmoptionSepArg, 0 },
270 { "-fg", ".foreground", XrmoptionSepArg, 0 },
271 { "-background", ".background", XrmoptionSepArg, 0 },
272 { "-bg", ".background", XrmoptionSepArg, 0 },
275 // <xscreensaver-updater />
276 { "-" SUSUEnableAutomaticChecksKey,
277 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" },
278 { "-no-" SUSUEnableAutomaticChecksKey,
279 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
280 { "-" SUAutomaticallyUpdateKey,
281 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" },
282 { "-no-" SUAutomaticallyUpdateKey,
283 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
284 { "-" SUSendProfileInfoKey,
285 "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
286 { "-no-" SUSendProfileInfoKey,
287 "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
288 { "-" SUScheduledCheckIntervalKey,
289 "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
290 # endif // !HAVE_IPHONE
294 { "-global-cycle", ".globalCycle", XrmoptionNoArg, "True" },
295 { "-no-global-cycle", ".globalCycle", XrmoptionNoArg, "False" },
296 { "-global-cycle-timeout", ".globalCycleTimeout", XrmoptionSepArg, 0 },
297 { "-global-cycle-selected", ".globalCycleSelected", XrmoptionNoArg,"True"},
298 { "-no-global-cycle-selected", ".globalCycleSelected",XrmoptionNoArg,
300 # endif // HAVE_IPHONE
304 static const char *default_defaults [] = {
306 # if defined(HAVE_IPHONE) && !defined(__OPTIMIZE__)
311 ".doubleBuffer: True",
312 ".multiSample: False",
316 ".textURL: https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
318 ".grabDesktopImages: yes",
320 ".chooseRandomImages: no",
322 ".chooseRandomImages: yes",
324 ".imageDirectory: ~/Pictures",
326 ".texFontCacheSize: 30",
330 # define STR(S) STR1(S)
331 # define __objc_yes Yes
332 # define __objc_no No
333 "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
334 "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef),
335 "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef),
336 "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef),
341 # endif // !HAVE_IPHONE
344 ".globalCycle: False",
345 ".globalCycleTimeout: 300",
346 ".globalCycleSelected: True",
347 # endif // HAVE_IPHONE
352 for (i = 0; default_options[i].option; i++)
354 for (i = 0; opts[i].option; i++)
357 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
358 calloc (count + 1, sizeof (*opts2));
362 while (default_options[j].option) {
363 opts2[i] = default_options[j];
367 while (opts[j].option) {
378 for (i = 0; default_defaults[i]; i++)
380 for (i = 0; defs[i]; i++)
383 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
387 while (default_defaults[j]) {
388 defs2[i] = default_defaults[j];
403 // This doesn't work. We display backtraces for exceptions, but not for
404 // signals, including for abort() unless it was wrapped with jwxyz_abort().
406 static void sighandler (int sig)
408 const char *s = strsignal(sig);
409 if (!s) s = "Unknown";
411 jwxyz_abort ("Signal: %s", s); // Throw NSException, show dialog
413 NSLog (@"Signal: %s", s); // Just make sure it is logged
415 // Log stack trace too.
416 // Same info shows up in Library/Logs/DiagnosticReports/ScreenSaverEngine*
419 int frames = backtrace (stack, countof(stack));
420 char **strs = backtrace_symbols (stack, frames);
421 NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
422 for (int i = 2; i < frames; i++) {
424 [backtrace addObject:[NSString stringWithUTF8String: strs[i]]];
426 // Can't embed newlines in the message for /usr/bin/log
427 NSLog(@"Stack:\\n\t%@", [backtrace componentsJoinedByString:@"\\n\t"]);
431 signal (sig, SIG_DFL);
432 kill (getpid (), sig);
437 catch_signal (int sig, void (*handler) (int))
440 a.sa_handler = handler;
441 sigemptyset (&a.sa_mask);
442 a.sa_flags = SA_NODEFER;
443 if (sigaction (sig, &a, 0) < 0)
446 sprintf (buf, "%s: couldn't catch signal %d", progname, sig);
451 static void catch_signals (void)
453 //catch_signal (SIGINT, sighandler); // shell ^C
454 //catch_signal (SIGQUIT, sighandler); // shell ^|
455 catch_signal (SIGILL, sighandler);
456 catch_signal (SIGTRAP, sighandler);
457 catch_signal (SIGABRT, sighandler);
458 catch_signal (SIGEMT, sighandler);
459 catch_signal (SIGFPE, sighandler);
460 catch_signal (SIGBUS, sighandler);
461 catch_signal (SIGSEGV, sighandler);
462 catch_signal (SIGSYS, sighandler);
463 //catch_signal (SIGTERM, sighandler); // kill default
464 //catch_signal (SIGKILL, sighandler); // -9 untrappable
465 catch_signal (SIGXCPU, sighandler);
466 catch_signal (SIGXFSZ, sighandler);
467 NSLog (@"installed signal handlers");
470 #endif // CATCH_SIGNALS
472 #ifdef VENTURA_KLUDGE // Duplicated in Randomizer.m
473 static NSMutableArray *all_saver_views = NULL;
477 - (id) initWithFrame:(NSRect)frame
478 title:(NSString *)_title
480 randomizer:(BOOL)randomizer_p
482 if (! (self = [super initWithFrame:frame isPreview:p]))
485 saver_title = [_title retain];
486 # ifdef CATCH_SIGNALS
489 xsft = [self findFunctionTable: saver_title];
499 xsft->setup_cb (xsft, xsft->setup_arg);
501 /* The plist files for these preferences show up in
502 $HOME/Library/Preferences/ByHost/ in a file named like
503 "org.jwz.xscreensaver.<CLASSNAME>.<NUMBERS>.plist"
505 NSString *name = [NSString stringWithCString:xsft->progclass
506 encoding:NSUTF8StringEncoding];
507 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
508 [self setResourcesEnv:name];
509 [self loadCustomFonts];
511 XrmOptionDescRec *opts = 0;
512 const char **defs = 0;
513 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
514 prefsReader = [[PrefsReader alloc]
515 initWithName:name xrmKeys:opts defaults:defs];
517 // free (opts); // bah, we need these! #### leak!
518 xsft->options = opts;
520 progname = progclass = xsft->progclass;
524 # if !defined HAVE_IPHONE && defined JWXYZ_QUARTZ
525 // When the view fills the screen and double buffering is enabled, OS X will
526 // use page flipping for a minor CPU/FPS boost. In windowed mode, double
527 // buffering reduces the frame rate to 1/2 the screen's refresh rate.
528 double_buffered_p = !p; // isPreview
535 // So we can tell when we're docked.
536 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
537 # endif // !HAVE_TVOS
539 [self setBackgroundColor:[NSColor blackColor]];
540 # endif // HAVE_IPHONE
543 // Colorspaces and CGContexts only happen with non-GL hacks.
544 colorspace = CGColorSpaceCreateDeviceRGB ();
547 #ifdef VENTURA_KLUDGE
548 if (randomizer_p && !p) {
549 NSLog(@"skipping Ventura kludge: handled by Randomizer");
550 } else if (!p) { // isPreview
551 if (!all_saver_views) {
552 all_saver_views = [[NSMutableArray arrayWithCapacity:20] retain];
553 [NSTimer scheduledTimerWithTimeInterval: 1 // must be > 0
555 selector: @selector(venturaLaunchKludge:)
559 if (! [all_saver_views containsObject:self])
560 [all_saver_views addObject:self];
562 #endif // VENTURA_KLUDGE
564 # ifdef SONOMA_KLUDGE // Duplicated in Randomizer.m
566 /* Oct 2023, macOS 14.0: we get startAnimation on each screen, but
567 stopAnimation is never called, and our process (legacyScreenSaver)
568 never exits. This means that the screen saver just keeps running
569 forever in the background on an invisible window, burning CPU!
571 That invisible window is both 'visible' and 'onActiveSpace', and has
572 no parentWindow, so its invisibility is not detectable.
574 However, there is a "com.apple.screensaver.willstop" notification and
575 from that we can intuit that we should send ourselves stopAnimation.
577 Except, stopAnimation() isn't great, because it seems that sometimes
578 legacyScreenSaver holds on to old copies of this bundle and begins
579 animating them again -- so we have multiple invisible copies of the
580 same saver running, which burns CPU uselessly and kills our frame rate.
581 So let's just exit() instead.
583 if (!p && !randomizer_p) {
584 [[NSDistributedNotificationCenter defaultCenter]
585 addObserverForName: @"com.apple.screensaver.willstop"
588 usingBlock:^(NSNotification *n) {
589 NSLog (@"received %@", [n name]);
590 [self stopAnimation];
592 [[NSApplication sharedApplication] terminate:self];
595 /* Do it before sleeping as well, I guess? */
596 [[[NSWorkspace sharedWorkspace] notificationCenter]
597 addObserverForName: NSWorkspaceWillSleepNotification
600 usingBlock:^(NSNotification *n) {
601 NSLog (@"received %@", [n name]);
602 [self stopAnimation];
605 # endif // SONOMA_KLUDGE
612 /* On 10.15, if "use random screen saver" is checked, then startAnimation
613 is never called. This may be related to Apple's buggy code in
614 ScreenSaverEngine calling nonexistent beginExtensionRequestWithUserInfo,
615 but on 10.15 we're not even running in that process: now we're in the
616 not-at-all-ominously-named legacyScreenSaver process.
618 Dec 2020, noticed that this also happens on 10.14.6 when *not* in random
619 mode. Both System Preferences and ScreenSaverEngine fail to call
622 June 2023, macOS 13.4: On a system with 3 screens, initWithFrame is called
623 on every screen, but viewDidMoveToWindow is called only on screen 3 -- but
624 that screen's view has the frame of screen 1! So we get only one saver
625 running, and it is the wrong size. We detect and correct this insanity
626 with the VENTURA_KLUDGE stuff.
628 - (void) viewDidMoveToWindow
631 self.window.frame.size.width > 0 &&
632 self.window.frame.size.height > 0)
633 [self startAnimation];
636 - (void) viewWillMoveToWindow:(NSWindow *)window
639 [self stopAnimation];
641 #endif // HAVE_IPHONE
643 #ifdef VENTURA_KLUDGE
644 /* Correct the insane shit that LegacyScreenSaver is throwing at us now.
645 We keep track of each ScreenSaverView that was created; and then a little
646 while after startup, we check to see which of those views have not been
647 attached to windows, or have the wrong geometry.
648 Duplicated in XScreenSaverView.m.
650 - (void) venturaLaunchKludge: (NSTimer *) timer
652 NSArray<NSWindow *> *windows = [NSApplication sharedApplication].windows;
654 const char *tag = "Ventura kludge";
656 // First log what was wrong.
659 for (NSWindow *w in windows) {
663 // Find the XScreenSaverView on this window.
664 for (NSView *v1 in all_saver_views) {
665 if (w.contentView == v1.superview) {
672 NSLog (@"%s: screen %d %gx%g+%g+%g had no saver view",
674 w.frame.size.width, w.frame.size.height,
675 w.frame.origin.x, w.frame.origin.y);
677 NSRect target = w.frame;
680 if (v.frame.size.width == target.size.width &&
681 v.frame.size.height == target.size.height &&
682 v.frame.origin.x == target.origin.x &&
683 v.frame.origin.y == target.origin.y) {
684 NSLog (@"%s: screen %d %gx%g+%g+%g had correct view frame"
687 w.frame.size.width, w.frame.size.height,
688 w.frame.origin.x, w.frame.origin.y,
689 target.size.width, target.size.height,
690 target.origin.x, target.origin.y);
692 NSLog (@"%s: screen %d %gx%g+%g+%g had view frame"
693 " %gx%g+%g+%g instead of %gx%g+%g+%g",
695 w.frame.size.width, w.frame.size.height,
696 w.frame.origin.x, w.frame.origin.y,
697 v.frame.size.width, v.frame.size.height,
698 v.frame.origin.x, v.frame.origin.y,
699 target.size.width, target.size.height,
700 target.origin.x, target.origin.y);
708 for (NSWindow *w in windows) {
712 // Find the XScreenSaverView on this window.
713 for (NSView *v1 in all_saver_views) {
714 if (w.contentView == v1.superview) {
720 BOOL attached_p = FALSE;
722 // This window has no ScreenSaverView. Pick any unattached one.
723 for (NSView *v1 in all_saver_views) {
726 NSLog (@"%s: screen %d %gx%g+%g+%g: attaching saver view",
728 w.frame.size.width, w.frame.size.height,
729 w.frame.origin.x, w.frame.origin.y);
731 [w.contentView addSubview: v];
738 // A view is attached to this window, but the frame might have the
739 // wrong size or origin.
740 NSRect target = w.frame;
743 if (v.frame.size.width != target.size.width ||
744 v.frame.size.height != target.size.height ||
745 v.frame.origin.x != target.origin.x ||
746 v.frame.origin.y != target.origin.y) {
748 NSLog (@"%s: screen %d %gx%g+%g+%g: correcting frame: "
749 "%gx%g+%g+%g => %gx%g+%g+%g",
751 w.frame.size.width, w.frame.size.height,
752 w.frame.origin.x, w.frame.origin.y,
753 v.frame.size.width, v.frame.size.height,
754 v.frame.origin.x, v.frame.origin.y,
755 target.size.width, target.size.height,
756 target.origin.x, target.origin.y);
757 [v setFrame: target];
762 #endif // VENTURA_KLUDGE
768 return [CAEAGLLayer class];
773 - (id) initWithFrame:(NSRect)f isPreview:(BOOL)p
775 return [self initWithFrame:f title:0 isPreview:p randomizer:FALSE];
778 - (id) initWithFrame:(NSRect)f title:(NSString*)t isPreview:(BOOL)p
780 return [self initWithFrame:f title:t isPreview:p randomizer:FALSE];
783 - (id) initWithFrame:(NSRect)f isPreview:(BOOL)p randomizer:(BOOL)r
785 return [self initWithFrame:f title:0 isPreview:p randomizer:r];
791 if ([self isAnimating])
792 [self stopAnimation];
793 NSAssert(!xdata, @"xdata not yet freed");
794 NSAssert(!xdpy, @"xdpy not yet freed");
797 [[NSNotificationCenter defaultCenter] removeObserver:self];
800 # ifdef BACKBUFFER_OPENGL
803 # endif // !HAVE_IPHONE
805 // Releasing the OpenGL context should also free any OpenGL objects,
806 // including the backbuffer texture and frame/render/depthbuffers.
807 # endif // BACKBUFFER_OPENGL
809 # if defined JWXYZ_GL && defined HAVE_IPHONE
810 [ogl_ctx_pixmap release];
815 CGColorSpaceRelease (colorspace);
816 # endif // JWXYZ_QUARTZ
818 [prefsReader release];
826 - (PrefsReader *) prefsReader
833 - (void) lockFocus { }
834 - (void) unlockFocus { }
835 #endif // HAVE_IPHONE
840 /* A few seconds after the saver launches, we store the "wasRunning"
841 preference. This is so that if the saver is crashing at startup,
842 we don't launch it again next time, getting stuck in a crash loop.
844 - (void) allSystemsGo: (NSTimer *) timer
846 NSAssert (timer == crash_timer, @"crash timer screwed up");
849 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
850 [prefs setBool:YES forKey:@"wasRunning"];
860 CGSize screen_size = self.bounds.size;
861 double s = self.contentScaleFactor;
862 screen_size.width *= s;
863 screen_size.height *= s;
866 GLuint *framebuffer = &xwindow->gl_framebuffer;
867 GLuint *renderbuffer = &xwindow->gl_renderbuffer;
868 xwindow->window.current_drawable = xwindow;
869 #elif defined JWXYZ_QUARTZ
870 GLuint *framebuffer = &gl_framebuffer;
871 GLuint *renderbuffer = &gl_renderbuffer;
872 #endif // JWXYZ_QUARTZ
874 if (*framebuffer) glDeleteFramebuffersOES (1, framebuffer);
875 if (*renderbuffer) glDeleteRenderbuffersOES (1, renderbuffer);
877 create_framebuffer (framebuffer, renderbuffer);
880 // glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES,
881 // (int)size.width, (int)size.height);
882 [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
883 fromDrawable:(CAEAGLLayer*)self.layer];
885 glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,
886 GL_RENDERBUFFER_OES, *renderbuffer);
888 [self addExtraRenderbuffers:screen_size];
890 check_framebuffer_status();
892 #endif // HAVE_IPHONE
895 - (void) startAnimation
897 if ([self isAnimating]) return; // macOS 10.15 stupidity
899 NSAssert(![self isAnimating], @"already animating");
900 NSAssert(!initted_p && !xdata, @"already initialized");
902 // See comment in render_x11() for why this value is important:
903 [self setAnimationTimeInterval: 1.0 / 240.0];
905 [super startAnimation];
906 /* We can't draw on the window from this method, so we actually do the
907 initialization of the screen saver (xsft->init_cb) in the first call
908 to animateOneFrame() instead.
913 [crash_timer invalidate];
915 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
916 [prefs removeObjectForKey:@"wasRunning"];
919 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
921 selector:@selector(allSystemsGo:)
926 [cycle_timer invalidate];
928 # endif // HAVE_IPHONE
930 // Never automatically turn the screen off if we are docked,
931 // and an animation is running.
935 [UIApplication sharedApplication].idleTimerDisabled =
936 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
937 # endif // !HAVE_TVOS
940 xwindow = (Window) calloc (1, sizeof(*xwindow));
941 xwindow->type = WINDOW;
942 xwindow->window.view = self;
943 CFRetain (xwindow->window.view); // needed for garbage collection?
945 #ifdef BACKBUFFER_OPENGL
946 CGSize new_backbuffer_size;
952 pixfmt = [self getGLPixelFormat];
955 NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
957 // Fun: On OS X 10.7, the second time an OpenGL context is created, after
958 // the preferences dialog is launched in SaverTester, the context only
959 // lasts until the first full GC. Then it turns black. Solution is to
960 // reuse the OpenGL context after this point.
961 // "Analyze" says that both pixfmt and ogl_ctx are leaked.
962 ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
965 // Sync refreshes to the vertical blanking interval
967 [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
968 // check_gl_error ("NSOpenGLCPSwapInterval"); // SEGV sometimes. Too early?
971 [ogl_ctx makeCurrentContext];
972 check_gl_error ("makeCurrentContext");
974 // NSOpenGLContext logs an 'invalid drawable' when this is called
975 // from initWithFrame.
976 [ogl_ctx setView:self];
978 // Get device pixels instead of points.
979 self.wantsBestResolutionOpenGLSurface = YES;
981 // This may not be necessary if there's FBO support.
983 xwindow->window.pixfmt = pixfmt;
984 CFRetain (xwindow->window.pixfmt);
985 xwindow->window.virtual_screen = [ogl_ctx currentVirtualScreen];
986 xwindow->window.current_drawable = xwindow;
987 NSAssert (ogl_ctx, @"no CGContext");
990 // Clear frame buffer ASAP, else there are bits left over from other apps.
991 glClearColor (0, 0, 0, 1);
992 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
994 // glXSwapBuffers (mi->dpy, mi->window);
997 // Enable multi-threading, if possible. This runs most OpenGL commands
998 // and GPU management on a second CPU.
1000 # ifndef kCGLCEMPEngine
1001 # define kCGLCEMPEngine 313 // Added in MacOS 10.4.8 + XCode 2.4.
1003 CGLContextObj cctx = CGLGetCurrentContext();
1004 CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
1005 if (err != kCGLNoError) {
1006 NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
1010 new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
1012 // Scale factor for desktop retina displays
1013 double s = [self hackedContentScaleFactor];
1014 new_backbuffer_size.width *= s;
1015 new_backbuffer_size.height *= s;
1017 # else // HAVE_IPHONE
1019 CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
1020 eagl_layer.opaque = TRUE;
1021 eagl_layer.drawableProperties = [self getGLProperties];
1023 // Without this, the GL frame buffer is half the screen resolution!
1024 eagl_layer.contentsScale = [UIScreen mainScreen].scale;
1026 PrefsReader *prefs = [self prefsReader];
1027 BOOL gles3p = [prefs getBooleanResource:"prefersGLSL"];
1029 ogl_ctx = [[EAGLContext alloc] initWithAPI:
1031 ? kEAGLRenderingAPIOpenGLES3
1032 : kEAGLRenderingAPIOpenGLES1)];
1034 ogl_ctx_pixmap = [[EAGLContext alloc]
1035 initWithAPI:kEAGLRenderingAPIOpenGLES1
1036 sharegroup:ogl_ctx.sharegroup];
1039 eagl_layer.contentsGravity = [self getCAGravity];
1043 xwindow->window.ogl_ctx_pixmap = ogl_ctx_pixmap;
1046 [EAGLContext setCurrentContext: ogl_ctx];
1050 double s = [self hackedContentScaleFactor];
1051 new_backbuffer_size = self.bounds.size;
1052 new_backbuffer_size.width *= s;
1053 new_backbuffer_size.height *= s;
1055 # endif // HAVE_IPHONE
1058 xwindow->ogl_ctx = ogl_ctx;
1059 # ifndef HAVE_IPHONE
1060 CFRetain (xwindow->ogl_ctx);
1061 # endif // HAVE_IPHONE
1064 check_gl_error ("startAnimation");
1066 // NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
1067 // glGetString (GL_RENDERER), glGetString (GL_VERSION));
1069 [self enableBackbuffer:new_backbuffer_size];
1071 #endif // BACKBUFFER_OPENGL
1074 [self createBackbuffer:new_backbuffer_size];
1078 /* "The stopAnimation or startAnimation methods do not immediately start
1079 or stop animation. In particular, it is not safe to assume that your
1080 animateOneFrame method will not execute (or continue to execute) after
1081 you call stopAnimation." */
1084 - (void)stopAnimation {
1085 [self stopAnimationWithException: NULL];
1088 - (void)stopAnimationWithException: (NSException *) error
1090 // #### When 'error' exists I would like to do a dirtier shutdown that
1091 // kills subprocesses and resets things, but does not touch any more
1092 // graphics state. Not sure how to do that...
1096 [cycle_timer invalidate];
1098 # endif // HAVE_IPHONE
1100 if (![self isAnimating]) return; // macOS 10.15 stupidity
1104 [self lockFocus]; // in case something tries to draw from here
1105 [self prepareContext];
1107 /* All of the xlockmore hacks need to have their release functions
1108 called, or launching the same saver twice does not work. Also
1109 webcollage-cocoa needs it in order to kill the inferior webcollage
1110 processes (since the screen saver framework never generates a
1114 xsft->free_cb (xdpy, xwindow, xdata);
1117 jwxyz_quartz_free_display (xdpy);
1119 # if defined JWXYZ_GL && !defined HAVE_IPHONE
1120 CFRelease (xwindow->ogl_ctx);
1122 CFRelease (xwindow->window.view);
1126 // setup_p = NO; // #### wait, do we need this?
1133 [crash_timer invalidate];
1135 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1136 [prefs removeObjectForKey:@"wasRunning"];
1137 [prefs synchronize];
1138 # endif // HAVE_IPHONE
1140 [super stopAnimation];
1142 // When an animation is no longer running (e.g., looking at the list)
1143 // then it's ok to power off the screen when docked.
1146 [UIApplication sharedApplication].idleTimerDisabled = NO;
1149 // Without this, the GL frame stays on screen when switching tabs
1150 // in System Preferences.
1151 // (Or perhaps it used to. It doesn't seem to matter on 10.9.)
1153 # ifndef HAVE_IPHONE
1154 [NSOpenGLContext clearCurrentContext];
1155 # endif // !HAVE_IPHONE
1157 clear_gl_error(); // This hack is defunct, don't let this linger.
1159 # ifdef JWXYZ_QUARTZ
1160 CGContextRelease (backbuffer);
1164 munmap (backbuffer_data, backbuffer_len);
1165 backbuffer_data = NULL;
1171 - (NSOpenGLContext *) oglContext
1177 // #### maybe this could/should just be on 'lockFocus' instead?
1178 - (void) prepareContext
1182 [EAGLContext setCurrentContext:ogl_ctx];
1183 #else // !HAVE_IPHONE
1184 [ogl_ctx makeCurrentContext];
1185 // check_gl_error ("makeCurrentContext");
1186 #endif // !HAVE_IPHONE
1189 xwindow->window.current_drawable = xwindow;
1196 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
1198 fps_compute (fpst, 0, -1);
1203 /* Some of the older X11 savers look bad if a "pixel" is not a thing you can
1204 see. They expect big, chunky, luxurious 1990s pixels, and if they use
1205 "device" pixels on a Retina screen, everything just disappears.
1207 Retina iPads have 768x1024 point screens which are 1536x2048 pixels,
1208 2017 iMac screens are 5120x2880 in device pixels.
1210 This method is overridden in XScreenSaverGLView, since this kludge
1211 isn't necessary for GL programs, being resolution independent by
1214 - (CGFloat) hackedContentScaleFactor
1216 return [self hackedContentScaleFactor:FALSE];
1219 - (CGFloat) hackedContentScaleFactor:(BOOL)fonts_p
1222 CGFloat s = self.contentScaleFactor;
1224 CGFloat s = self.window.backingScaleFactor;
1227 /* This notion of "scale fonts differently than the viewport" seemed
1228 like it made sense for BSOD but it makes -fps text be stupidly
1229 large for all other hacks. So instead let's just make BSOD not
1230 be lowrez. There are no other lowrez hacks that make heavy use
1234 if (_lowrez_p && !fonts_p) {
1235 NSSize b = [self bounds].size; // This is in points, not pixels
1236 CGFloat wh = b.width > b.height ? b.width : b.height;
1237 wh *= s; // points -> pixels
1239 // Scale down to as close to 1024 as we can get without going under,
1240 // while keeping an integral scale factor so that we don't get banding
1241 // artifacts and moire patterns.
1243 // Retina sizes: 2208 => 1104, 2224 => 1112, 2732 => 1366, 2880 => 1440.
1256 current_device_rotation (void)
1260 # else // !HAVE_TVOS
1261 UIDeviceOrientation o = [[UIDevice currentDevice] orientation];
1263 /* Sometimes UIDevice doesn't know the proper orientation, or the device is
1264 face up/face down, so in those cases fall back to the status bar
1265 orientation. The SaverViewController tries to set the status bar to the
1266 proper orientation before it creates the XScreenSaverView; see
1267 _storedOrientation in SaverViewController.
1269 if (o == UIDeviceOrientationUnknown ||
1270 o == UIDeviceOrientationFaceUp ||
1271 o == UIDeviceOrientationFaceDown) {
1272 /* Mind the differences between UIInterfaceOrientation and
1273 UIDeviceOrientation:
1274 1. UIInterfaceOrientation does not include FaceUp and FaceDown.
1275 2. LandscapeLeft and LandscapeRight are swapped between the two. But
1276 converting between device and interface orientation doesn't need to
1277 take this into account, because (from the UIInterfaceOrientation
1278 description): "rotating the device requires rotating the content in
1279 the opposite direction."
1281 /* statusBarOrientation deprecated in iOS 9 */
1282 o = (UIDeviceOrientation) // from UIInterfaceOrientation
1283 [UIApplication sharedApplication].statusBarOrientation;
1287 case UIDeviceOrientationLandscapeLeft: return -90; break;
1288 case UIDeviceOrientationLandscapeRight: return 90; break;
1289 case UIDeviceOrientationPortraitUpsideDown: return 180; break;
1290 default: return 0; break;
1292 # endif // !HAVE_TVOS
1296 - (void) handleException: (NSException *)e
1298 NSLog (@"Caught exception: %@", e);
1299 UIAlertController *c =
1301 alertControllerWithTitle:
1302 [NSString stringWithFormat: @"%@ crashed!", saver_title]
1303 message: [NSString stringWithFormat:
1304 @"The error message was:"
1306 "If it keeps crashing, try resetting its options.",
1308 preferredStyle: UIAlertControllerStyleAlert];
1310 [c addAction: [UIAlertAction actionWithTitle:
1311 NSLocalizedString(@"Exit", @"")
1312 style: UIAlertActionStyleDefault
1313 handler: ^(UIAlertAction *a) {
1316 [c addAction: [UIAlertAction actionWithTitle:
1317 NSLocalizedString(@"Keep going", @"")
1318 style: UIAlertActionStyleDefault
1319 handler: ^(UIAlertAction *a) {
1320 [self stopAndClose:NO];
1323 UIViewController *vc =
1324 [UIApplication sharedApplication].keyWindow.rootViewController;
1325 while (vc.presentedViewController)
1326 vc = vc.presentedViewController;
1327 [vc presentViewController:c animated:YES completion:nil];
1328 [self stopAnimation];
1331 #endif // HAVE_IPHONE
1336 # ifndef HAVE_IPHONE
1340 // iOS always uses OpenGL ES 1.1.
1346 gl_check_ver (const struct gl_version *caps,
1350 return caps->major > gl_major ||
1351 (caps->major == gl_major && caps->minor >= gl_minor);
1356 /* Called during startAnimation before the first call to createBackbuffer. */
1357 - (void) enableBackbuffer:(CGSize)new_backbuffer_size
1359 # ifndef HAVE_IPHONE
1360 struct gl_version version;
1363 const char *version_str = (const char *)glGetString (GL_VERSION);
1365 if (! version_str) {
1366 NSLog (@"no GL_VERSION?");
1370 /* iPhone is always OpenGL ES 1.1. */
1371 if (sscanf ((const char *)version_str, "%u.%u",
1372 &version.major, &version.minor) < 2)
1380 // The OpenGL extensions in use in here are pretty are pretty much ubiquitous
1381 // on OS X, but it's still good form to check.
1382 const GLubyte *extensions = glGetString (GL_EXTENSIONS);
1384 glGenTextures (1, &backbuffer_texture);
1386 // On really old systems, it would make sense to split the texture
1388 # ifndef HAVE_IPHONE
1389 gl_texture_target = (gluCheckExtension ((const GLubyte *)
1390 "GL_ARB_texture_rectangle",
1392 ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
1394 // OES_texture_npot also provides this, but iOS never provides it.
1395 gl_limited_npot_p = jwzgles_gluCheckExtension
1396 ((const GLubyte *) "GL_APPLE_texture_2D_limited_npot", extensions);
1397 gl_texture_target = GL_TEXTURE_2D;
1400 glBindTexture (gl_texture_target, backbuffer_texture);
1401 glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1402 // GL_LINEAR might make sense on Retina iPads.
1403 glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1404 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1405 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1407 # ifndef HAVE_IPHONE
1408 // There isn't much sense in supporting one of these if the other
1410 gl_apple_client_storage_p =
1411 gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage",
1413 gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions);
1415 if (gl_apple_client_storage_p) {
1416 glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE,
1417 GL_STORAGE_SHARED_APPLE);
1418 glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
1422 // If a video adapter suports BGRA textures, then that's probably as fast as
1423 // you're gonna get for getting a texture onto the screen.
1426 jwzgles_gluCheckExtension
1427 ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888", extensions) ?
1431 gl_pixel_type = GL_UNSIGNED_BYTE;
1432 // See also OES_read_format.
1434 if (gl_check_ver (&version, 1, 2) ||
1435 (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) &&
1436 gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels",
1438 gl_pixel_format = GL_BGRA;
1439 // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV.
1440 gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1442 gl_pixel_format = GL_RGBA;
1443 gl_pixel_type = GL_UNSIGNED_BYTE;
1445 // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more
1446 // sense on PowerPC.
1449 glEnable (gl_texture_target);
1450 glEnableClientState (GL_VERTEX_ARRAY);
1451 glEnableClientState (GL_TEXTURE_COORD_ARRAY);
1453 check_gl_error ("enableBackbuffer");
1458 - (BOOL) suppressRotationAnimation
1460 return [self ignoreRotation]; // Don't animate if we aren't rotating
1463 - (BOOL) rotateTouches
1465 return FALSE; // Adjust event coordinates only if rotating
1470 - (void) setViewport
1472 # ifdef BACKBUFFER_OPENGL
1473 NSAssert ([NSOpenGLContext currentContext] ==
1474 ogl_ctx, @"invalid GL context");
1476 NSSize new_size = self.bounds.size;
1479 GLfloat s = self.contentScaleFactor;
1480 # else // !HAVE_IPHONE
1481 const GLfloat s = self.window.backingScaleFactor;
1483 GLfloat hs = self.hackedContentScaleFactor;
1485 // On OS X this almost isn't necessary, except for the ugly aliasing
1487 glViewport (0, 0, new_size.width * s, new_size.height * s);
1489 glMatrixMode (GL_PROJECTION);
1496 (-new_size.width * hs, new_size.width * hs,
1497 -new_size.height * hs, new_size.height * hs,
1501 if ([self ignoreRotation]) {
1502 int o = (int) -current_device_rotation();
1503 glRotatef (o, 0, 0, 1);
1505 # endif // HAVE_IPHONE
1506 # endif // BACKBUFFER_OPENGL
1510 /* Create a bitmap context into which we render everything.
1511 If the desired size has changed, re-created it.
1512 new_size is in rotated pixels, not points: the same size
1513 and shape as the X11 window as seen by the hacks.
1515 - (void) createBackbuffer:(CGSize)new_size
1517 CGSize osize = CGSizeZero;
1519 osize.width = CGBitmapContextGetWidth(backbuffer);
1520 osize.height = CGBitmapContextGetHeight(backbuffer);
1524 (int)osize.width == (int)new_size.width &&
1525 (int)osize.height == (int)new_size.height)
1528 CGContextRef ob = backbuffer;
1529 void *odata = backbuffer_data;
1530 GLsizei olen = backbuffer_len;
1532 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1533 NSLog(@"backbuffer %.0fx%.0f", new_size.width, new_size.height);
1536 /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
1537 <https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html>.
1539 iOS uses bog-standard glTexImage2D (for now).
1541 glMapBuffer is the standard way to get data from system RAM to video
1542 memory asynchronously and without a memcpy, but support for
1543 APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer),
1544 and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3
1545 (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with
1546 CGBitmapContext: glMapBuffer can return a different pointer on each
1547 call, but a CGBitmapContext doesn't allow its data pointer to be
1548 changed -- and recreating the context for a new pointer can be
1549 expensive (glyph caches get dumped, for instance).
1551 glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT,
1552 and these seem to allow mapping the buffer and leaving it where it is
1553 in client address space while OpenGL works with the buffer, but it
1554 requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for
1555 GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage.
1557 AMD_pinned_buffer provides the same advantage as glMapBufferRange, but
1558 Apple never implemented that one for OS X.
1561 backbuffer_data = NULL;
1562 gl_texture_w = (int)new_size.width;
1563 gl_texture_h = (int)new_size.height;
1565 NSAssert (gl_texture_target == GL_TEXTURE_2D
1566 # ifndef HAVE_IPHONE
1567 || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT
1569 , @"unexpected GL texture target");
1571 # ifndef HAVE_IPHONE
1572 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1574 if (!gl_limited_npot_p)
1577 gl_texture_w = (GLsizei) to_pow2 (gl_texture_w);
1578 gl_texture_h = (GLsizei) to_pow2 (gl_texture_h);
1581 GLsizei bytes_per_row = gl_texture_w * 4;
1583 # if defined(BACKBUFFER_OPENGL) && !defined(HAVE_IPHONE)
1584 // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
1585 // it will fall back to a memcpy.
1586 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24
1587 bytes_per_row = (bytes_per_row + 31) & ~31;
1588 # endif // BACKBUFFER_OPENGL && !HAVE_IPHONE
1590 backbuffer_len = bytes_per_row * gl_texture_h;
1591 if (backbuffer_len) // mmap requires this to be non-zero.
1592 backbuffer_data = mmap (NULL, backbuffer_len,
1593 PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED,
1596 BOOL alpha_first_p, order_little_p;
1598 if (gl_pixel_format == GL_BGRA) {
1599 alpha_first_p = YES;
1600 order_little_p = YES;
1602 } else if (gl_pixel_format == GL_ABGR_EXT) {
1604 order_little_p = YES; */
1606 NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format");
1608 order_little_p = NO;
1612 NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type");
1614 NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 ||
1615 gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV ||
1616 gl_pixel_type == GL_UNSIGNED_BYTE,
1617 @"unknown GL pixel type");
1619 #if defined __LITTLE_ENDIAN__
1620 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8;
1621 #elif defined __BIG_ENDIAN__
1622 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1624 # error Unknown byte order.
1627 if (gl_pixel_type == backwards_pixel_type)
1628 order_little_p ^= YES;
1631 CGBitmapInfo bitmap_info =
1632 (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) |
1633 (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
1635 backbuffer = CGBitmapContextCreate (backbuffer_data,
1636 (int)new_size.width,
1637 (int)new_size.height,
1642 NSAssert (backbuffer, @"unable to allocate back buffer");
1646 r.origin.x = r.origin.y = 0;
1648 CGContextSetGrayFillColor (backbuffer, 0, 1);
1649 CGContextFillRect (backbuffer, r);
1651 # if defined(BACKBUFFER_OPENGL) && !defined(HAVE_IPHONE)
1652 if (gl_apple_client_storage_p)
1653 glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data);
1654 # endif // BACKBUFFER_OPENGL && !HAVE_IPHONE
1657 // Restore old bits, as much as possible, to the X11 upper left origin.
1659 CGRect rect; // pixels, not points
1661 rect.origin.y = (new_size.height - osize.height);
1664 CGImageRef img = CGBitmapContextCreateImage (ob);
1665 CGContextDrawImage (backbuffer, rect, img);
1666 CGImageRelease (img);
1667 CGContextRelease (ob);
1670 // munmap should round len up to the nearest page.
1671 munmap (odata, olen);
1674 check_gl_error ("createBackbuffer");
1678 - (void) drawBackbuffer
1680 # ifdef BACKBUFFER_OPENGL
1682 NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]],
1683 @"ogl_ctx is not an NSOpenGLContext");
1685 NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4),
1686 @"improperly-aligned backbuffer");
1688 // This gets width and height from the backbuffer in case
1689 // APPLE_client_storage is in use. See the note in createBackbuffer.
1690 // This still has to happen every frame even when APPLE_client_storage has
1691 // the video adapter pulling texture data straight from
1692 // XScreenSaverView-owned memory.
1693 glTexImage2D (gl_texture_target, 0, GL_RGBA,
1694 (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4),
1695 gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
1698 GLfloat w = xwindow->frame.width, h = xwindow->frame.height;
1700 GLfloat vertices[4][2] = {{-w, h}, {w, h}, {w, -h}, {-w, -h}};
1702 GLfloat tex_coords[4][2];
1704 # ifndef HAVE_IPHONE
1705 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1706 # endif // HAVE_IPHONE
1712 tex_coords[0][0] = 0;
1713 tex_coords[0][1] = 0;
1714 tex_coords[1][0] = w;
1715 tex_coords[1][1] = 0;
1716 tex_coords[2][0] = w;
1717 tex_coords[2][1] = h;
1718 tex_coords[3][0] = 0;
1719 tex_coords[3][1] = h;
1721 glVertexPointer (2, GL_FLOAT, 0, vertices);
1722 glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
1723 glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
1725 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1726 check_gl_error ("drawBackbuffer");
1728 # endif // BACKBUFFER_OPENGL
1731 #endif // JWXYZ_QUARTZ
1735 - (void)enableBackbuffer:(CGSize)new_backbuffer_size;
1737 jwxyz_set_matrices (new_backbuffer_size.width, new_backbuffer_size.height);
1738 check_gl_error ("enableBackbuffer");
1741 - (void)createBackbuffer:(CGSize)new_size
1743 NSAssert ([NSOpenGLContext currentContext] ==
1744 ogl_ctx, @"invalid GL context");
1745 NSAssert (xwindow->window.current_drawable == xwindow,
1746 @"current_drawable not set properly");
1748 # ifndef HAVE_IPHONE
1749 /* On iOS, Retina means glViewport gets called with the screen size instead
1750 of the backbuffer/xwindow size. This happens in startAnimation.
1752 The GL screenhacks call glViewport themselves.
1754 glViewport (0, 0, new_size.width, new_size.height);
1757 // TODO: Preserve contents on resize.
1758 glClear (GL_COLOR_BUFFER_BIT);
1759 check_gl_error ("createBackbuffer");
1765 - (void)flushBackbuffer
1768 // Make sure the right context is active: there's two under JWXYZ_GL.
1769 jwxyz_bind_drawable (xwindow, xwindow);
1772 # ifndef HAVE_IPHONE
1774 # ifdef JWXYZ_QUARTZ
1775 // The OpenGL pipeline is not automatically synchronized with the contents
1776 // of the backbuffer, so without glFinish, OpenGL can start rendering from
1777 // the backbuffer texture at the same time that JWXYZ is clearing and
1778 // drawing the next frame in the backing store for the backbuffer texture.
1779 // This is only a concern under JWXYZ_QUARTZ because of
1780 // APPLE_client_storage; JWXYZ_GL doesn't use that.
1782 # endif // JWXYZ_QUARTZ
1784 // If JWXYZ_GL was single-buffered, there would need to be a glFinish (or
1785 // maybe just glFlush?) here, because single-buffered contexts don't always
1786 // update what's on the screen after drawing finishes. (i.e., in safe mode)
1788 # ifdef JWXYZ_QUARTZ
1789 // JWXYZ_GL is always double-buffered.
1790 if (double_buffered_p)
1791 # endif // JWXYZ_QUARTZ
1792 [ogl_ctx flushBuffer]; // despite name, this actually swaps
1793 # else // HAVE_IPHONE
1795 // jwxyz_bind_drawable() only binds the framebuffer, not the renderbuffer.
1797 GLint gl_renderbuffer = xwindow->gl_renderbuffer;
1800 glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
1801 [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
1802 # endif // HAVE_IPHONE
1804 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1805 // glGetError waits for the OpenGL command pipe to flush, so skip it in
1807 // OpenGL Programming Guide for Mac -> OpenGL Application Design
1808 // Strategies -> Allow OpenGL to Manage Your Resources
1809 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7
1810 check_gl_error ("flushBackbuffer");
1815 /* Inform X11 that the size of our window has changed.
1819 if (!xdpy) return; // early
1821 NSSize new_size; // pixels, not points
1823 new_size = self.bounds.size;
1827 // If this hack ignores rotation, then that means that it pretends to
1828 // always be in portrait mode. If the View has been resized to a
1829 // landscape shape, swap width and height to keep the backbuffer
1832 double rot = current_device_rotation();
1833 if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
1834 CGFloat swap = new_size.width;
1835 new_size.width = new_size.height;
1836 new_size.height = swap;
1838 # endif // HAVE_IPHONE
1840 double s = self.hackedContentScaleFactor;
1841 new_size.width *= s;
1842 new_size.height *= s;
1844 [self prepareContext];
1847 // On first resize, xwindow->frame is 0x0.
1848 if (xwindow->frame.width == new_size.width &&
1849 xwindow->frame.height == new_size.height)
1852 # if defined(BACKBUFFER_OPENGL) && !defined(HAVE_IPHONE)
1854 # endif // BACKBUFFER_OPENGL && !HAVE_IPHONE
1856 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
1857 xwindow->frame.x = 0;
1858 xwindow->frame.y = 0;
1859 xwindow->frame.width = new_size.width;
1860 xwindow->frame.height = new_size.height;
1862 [self createBackbuffer:CGSizeMake(xwindow->frame.width,
1863 xwindow->frame.height)];
1865 # if defined JWXYZ_QUARTZ
1866 xwindow->cgc = backbuffer;
1867 NSAssert (xwindow->cgc, @"no CGContext");
1868 # elif defined JWXYZ_GL && !defined HAVE_IPHONE
1870 [ogl_ctx setView:xwindow->window.view]; // (Is this necessary?)
1871 # endif // JWXYZ_GL && HAVE_IPHONE
1873 jwxyz_window_resized (xdpy);
1875 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1876 NSLog(@"reshape %.0fx%.0f %.1fx", new_size.width, new_size.height, s);
1879 // Next time render_x11 is called, run the saver's reshape_cb.
1886 /* Called by SaverRunner when the device has changed orientation.
1887 That means we need to generate a resize event, even if the size
1888 has not changed (e.g., from LandscapeLeft to LandscapeRight).
1890 - (void) orientationChanged
1894 next_frame_time = 0; // Get a new frame on screen quickly
1897 /* A hook run after the 'reshape_' method has been called. Used by
1898 XScreenSaverGLView to adjust the in-scene GL viewport.
1900 - (void) postReshape
1903 #endif // HAVE_IPHONE
1906 // Only render_x11 should call this. XScreenSaverGLView specializes it.
1907 - (void) reshape_x11
1909 xsft->reshape_cb (xdpy, xwindow, xdata,
1910 xwindow->frame.width, xwindow->frame.height);
1919 // jwxyz_make_display needs this.
1920 [self prepareContext]; // resize_x11 also calls this.
1927 # ifdef JWXYZ_QUARTZ
1928 xwindow->cgc = backbuffer;
1929 # endif // JWXYZ_QUARTZ
1930 xdpy = jwxyz_quartz_make_display (xwindow);
1932 # if defined HAVE_IPHONE
1933 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
1936 TRUE; // Rotation doesn't work yet. TODO: Make rotation work.
1938 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
1939 # endif // !JWXYZ_GL
1940 # endif // HAVE_IPHONE
1942 _lowrez_p = get_boolean_resource (xdpy, "lowrez", "Lowrez");
1946 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1947 NSSize b = [self bounds].size;
1948 CGFloat s = self.hackedContentScaleFactor;
1950 CGFloat o = self.contentScaleFactor;
1952 CGFloat o = self.window.backingScaleFactor;
1956 NSLog(@"lowrez: scaling %.0fx%.0f -> %.0fx%.0f (%.02f)",
1957 b.width * o, b.height * o,
1958 b.width * s, b.height * s, s);
1968 xsft->setup_cb (xsft, xsft->setup_arg);
1971 NSAssert(!xdata, @"xdata already initialized");
1974 # undef ya_rand_init
1977 XSetWindowBackground (xdpy, xwindow,
1978 get_pixel_resource (xdpy, 0,
1979 "background", "Background"));
1980 XClearWindow (xdpy, xwindow);
1982 # ifndef HAVE_IPHONE
1983 [[self window] setAcceptsMouseMovedEvents:YES];
1986 /* Kludge: even though the init_cb functions are declared to take 2 args,
1987 actually call them with 3, for the benefit of xlockmore_init() and
1990 void *(*init_cb) (Display *, Window, void *) =
1991 (void *(*) (Display *, Window, void *)) xsft->init_cb;
1993 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
1994 // NSAssert(xdata, @"no xdata from init");
1995 if (! xdata) abort();
1997 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1998 fpst = fps_init (xdpy, xwindow);
1999 fps_cb = xsft->fps_cb;
2000 if (! fps_cb) fps_cb = screenhack_do_fps;
2006 if (current_device_rotation() != 0) // launched while rotated
2010 # ifndef HAVE_IPHONE
2011 [NSTimer scheduledTimerWithTimeInterval: 10 + frand(10)
2013 selector: @selector(checkForUpdates)
2016 # endif // !HAVE_IPHONE
2019 BOOL cyclep = get_boolean_resource (xdpy, "globalCycle", "GlobalCycle");
2022 ? get_integer_resource(xdpy, "globalCycleTimeout", "GlobalCycleTimeout")
2024 NSLog (@"cycle_sec = %d", cycle_sec);
2026 cycle_timer = [NSTimer scheduledTimerWithTimeInterval: cycle_sec
2028 selector:@selector(cycleSaver)
2031 # endif // HAVE_IPHONE
2035 /* I don't understand why we have to do this *every frame*, but we do,
2036 or else the cursor comes back on.
2038 # ifndef HAVE_IPHONE
2039 if (![self isPreview])
2040 [NSCursor setHiddenUntilMouseMoves:YES];
2046 /* This is just a guess, but the -fps code wants to know how long
2047 we were sleeping between frames.
2049 long usecs = 1000000 * [self animationTimeInterval];
2050 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
2051 if (usecs < 0) usecs = 0;
2052 fps_slept (fpst, usecs);
2056 /* Run any XtAppAddInput and XtAppAddTimeOut callbacks now.
2057 Do this before delaying for next_frame_time to avoid throttling
2058 timers to the hack's frame rate.
2060 XtAppProcessEvent (XtDisplayToApplicationContext (xdpy),
2061 XtIMTimer | XtIMAlternateInput);
2064 /* It turns out that on some systems (possibly only 10.5 and older?)
2065 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
2066 that we cannot rely on it.
2068 Some of the screen hacks want to delay for long periods, and letting the
2069 framework run the update function at 30 FPS when it really wanted half a
2070 minute between frames would be bad. So instead, we assume that the
2071 framework's animation timer might fire whenever, but we only invoke the
2072 screen hack's "draw frame" method when enough time has expired.
2074 This means two extra calls to gettimeofday() per frame. For fast-cycling
2075 screen savers, that might actually slow them down. Oh well.
2077 A side-effect of this is that it's not possible for a saver to request
2078 an animation interval that is faster than animationTimeInterval.
2080 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
2081 ignored, it's important that it be faster than 30 FPS. 240 FPS is good.
2083 An NSTimer won't fire if the timer is already running the invocation
2084 function from a previous firing. So, if we use a 30 FPS
2085 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
2086 frame, there will be a 26666 µs delay until the next frame, 66666 µs
2087 after the beginning of the current frame. In other words, 25 FPS
2090 Frame rates tend to snap to values of 30/N, where N is a positive
2091 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
2092 is rounded down from what it would normally be.
2094 So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates
2095 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
2096 steps for higher or lower animation time intervals respectively.
2099 gettimeofday (&tv, 0);
2100 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
2101 if (now < next_frame_time) return;
2103 // [self flushBackbuffer];
2106 // We do this here instead of in setFrame so that all the
2107 // Xlib drawing takes place under the animation timer.
2109 # ifndef HAVE_IPHONE
2111 [ogl_ctx setView:self];
2112 # endif // !HAVE_IPHONE
2121 // NSAssert(xdata, @"no xdata when drawing");
2122 if (! xdata) abort();
2123 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
2125 fps_cb (xdpy, xwindow, fpst, xdata);
2127 gettimeofday (&tv, 0);
2128 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
2129 next_frame_time = now + (delay / 1000000.0);
2131 # ifdef JWXYZ_QUARTZ
2132 [self drawBackbuffer];
2134 // This can also happen near the beginning of render_x11.
2135 [self flushBackbuffer];
2137 # ifdef HAVE_IPHONE // Allow savers on the iPhone to run full-tilt.
2138 if (delay < [self animationTimeInterval])
2139 [self setAnimationTimeInterval:(delay / 1000000.0)];
2144 @catch (NSException *e) {
2145 [self handleException: e];
2147 # endif // HAVE_IPHONE
2151 static int frame = 0;
2152 if (++frame == 100) {
2153 fprintf(stderr,"BOOM\n");
2154 // int aa = *((int*)y);
2162 /* drawRect always does nothing, and animateOneFrame renders bits to the
2163 screen. This is (now) true of both X11 and GL on both MacOS and iOS.
2164 But this null method needs to exist or things complain.
2166 Note that drawRect is called before startAnimation, with the intent
2167 that it draws the initial state to be exposed by the fade-in.
2169 - (void)drawRect:(NSRect)rect
2174 - (void) animateOneFrame
2176 // Render X11 into the backing store bitmap...
2178 # ifdef JWXYZ_QUARTZ
2179 NSAssert (backbuffer, @"no back buffer");
2182 UIGraphicsPushContext (backbuffer);
2184 # endif // JWXYZ_QUARTZ
2188 # if defined HAVE_IPHONE && defined JWXYZ_QUARTZ
2189 UIGraphicsPopContext();
2200 # ifndef HAVE_IPHONE // Doesn't exist on iOS
2202 - (void) setFrame:(NSRect) newRect
2204 [super setFrame:newRect];
2206 if (xwindow) // inform Xlib that the window has changed now.
2210 - (void) setFrameSize:(NSSize) newSize
2212 [super setFrameSize:newSize];
2217 # else // HAVE_IPHONE
2219 - (void) layoutSubviews
2221 [super layoutSubviews];
2230 +(BOOL) performGammaFade
2235 - (BOOL) hasConfigureSheet
2240 + (NSString *) decompressXML: (NSData *)data
2242 if (! data) return 0;
2243 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
2245 // If it's not already XML, decompress it.
2246 NSAssert (compressed_p, @"xml isn't compressed");
2248 NSMutableData *data2 = 0;
2251 memset (&zs, 0, sizeof(zs));
2252 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
2254 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
2255 data2 = [NSMutableData dataWithLength: usize];
2256 zs.next_in = (Bytef *) data.bytes;
2257 zs.avail_in = (uint) data.length;
2258 zs.next_out = (Bytef *) data2.bytes;
2259 zs.avail_out = (uint) data2.length;
2260 ret = inflate (&zs, Z_FINISH);
2263 if (ret == Z_OK || ret == Z_STREAM_END)
2266 NSAssert2 (0, @"gunzip error: %d: %s",
2267 ret, (zs.msg ? zs.msg : "<null>"));
2270 NSString *s = [[NSString alloc]
2271 initWithData:data encoding:NSUTF8StringEncoding];
2278 - (NSWindow *) configureSheet
2280 - (UIViewController *) configureView
2283 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2284 NSString *classname = [NSString stringWithCString:xsft->progclass
2285 encoding:NSUTF8StringEncoding];
2286 NSString *file = [classname lowercaseString];
2287 NSString *path = [bundle pathForResource:file ofType:@"xml"];
2289 NSLog (@"%@.xml does not exist in the application bundle: %@/",
2290 file, [bundle resourcePath]);
2295 UIViewController *sheet;
2296 NSString *updater = 0;
2297 # else // !HAVE_IPHONE
2299 NSString *updater = [self updaterPath];
2300 # endif // !HAVE_IPHONE
2303 NSData *xmld = [NSData dataWithContentsOfFile:path];
2304 NSString *xml = [[self class] decompressXML: xmld];
2305 sheet = [[XScreenSaverConfigSheet alloc]
2306 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
2308 options:xsft->options
2309 controller:[prefsReader userDefaultsController]
2310 globalController:[prefsReader globalDefaultsController]
2311 defaults:[prefsReader defaultOptions]
2312 haveUpdater:(updater ? TRUE : FALSE)];
2314 // #### am I expected to retain this, or not? wtf.
2315 // I thought not, but if I don't do this, we (sometimes) crash.
2316 // #### Analyze says "potential leak of an object stored into sheet"
2323 - (NSUserDefaultsController *) userDefaultsController
2325 return [prefsReader userDefaultsController];
2329 /* Announce our willingness to accept keyboard input.
2331 - (BOOL)acceptsFirstResponder
2339 # ifndef HAVE_IPHONE
2341 # else // HAVE_IPHONE
2343 // There's no way to play a standard system alert sound!
2344 // We'd have to include our own WAV for that.
2346 // Or we could vibrate:
2347 // #import <AudioToolbox/AudioToolbox.h>
2348 // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
2350 // Instead, just flash the screen white, then fade.
2352 UIView *v = [[UIView alloc] initWithFrame: [self frame]];
2353 [v setBackgroundColor: [UIColor whiteColor]];
2354 [[self window] addSubview:v];
2355 [UIView animateWithDuration: 0.1
2356 animations:^{ [v setAlpha: 0.0]; }
2357 completion:^(BOOL finished) { [v removeFromSuperview]; } ];
2359 # endif // HAVE_IPHONE
2363 /* Send an XEvent to the hack. Returns YES if it was handled.
2365 - (BOOL) sendEvent: (XEvent *) e
2367 if (!initted_p || ![self isAnimating]) // no event handling unless running.
2370 // [self lockFocus]; // As of 10.14 this causes flicker on mouse motion
2371 [self prepareContext];
2372 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
2373 // [self unlockFocus];cp -Rf ${CONFIGURATION_BUILD_DIR}/BuildOutputPrefPane.prefPane ~/Library/PreferencePanes
2380 /* Convert an NSEvent into an XEvent, and pass it along.
2381 Returns YES if it was handled.
2383 - (BOOL) convertEvent: (NSEvent *) e
2387 memset (&xe, 0, sizeof(xe));
2391 int flags = [e modifierFlags];
2392 if (flags & NSEventModifierFlagCapsLock) state |= LockMask;
2393 if (flags & NSEventModifierFlagShift) state |= ShiftMask;
2394 if (flags & NSEventModifierFlagControl) state |= ControlMask;
2395 if (flags & NSEventModifierFlagOption) state |= Mod1Mask;
2396 if (flags & NSEventModifierFlagCommand) state |= Mod2Mask;
2398 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
2400 double s = [self hackedContentScaleFactor];
2402 int y = s * ([self bounds].size.height - p.y);
2404 xe.xany.type = type;
2410 xe.xbutton.state = state;
2411 if ([e type] == NSEventTypeScrollWheel)
2412 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
2413 [e deltaY] < 0 ? Button5 :
2414 [e deltaX] > 0 ? Button6 :
2415 [e deltaX] < 0 ? Button7 :
2418 xe.xbutton.button = (unsigned int) [e buttonNumber] + 1;
2423 xe.xmotion.state = state;
2428 NSString *ns = (([e type] == NSEventTypeFlagsChanged) ? 0 :
2429 [e charactersIgnoringModifiers]);
2432 if (!ns || [ns length] == 0) // dead key
2434 // Cocoa hides the difference between left and right keys.
2435 // Also we only get KeyPress events for these, no KeyRelease
2436 // (unless we hack the mod state manually. Bleh.)
2438 if (flags & NSEventModifierFlagCapsLock) k = XK_Caps_Lock;
2439 else if (flags & NSEventModifierFlagShift) k = XK_Shift_L;
2440 else if (flags & NSEventModifierFlagControl) k = XK_Control_L;
2441 else if (flags & NSEventModifierFlagOption) k = XK_Alt_L;
2442 else if (flags & NSEventModifierFlagCommand) k = XK_Meta_L;
2444 else if ([ns length] == 1) // real key
2446 switch ([ns characterAtIndex:0]) {
2447 case NSLeftArrowFunctionKey: k = XK_Left; break;
2448 case NSRightArrowFunctionKey: k = XK_Right; break;
2449 case NSUpArrowFunctionKey: k = XK_Up; break;
2450 case NSDownArrowFunctionKey: k = XK_Down; break;
2451 case NSPageUpFunctionKey: k = XK_Page_Up; break;
2452 case NSPageDownFunctionKey: k = XK_Page_Down; break;
2453 case NSHomeFunctionKey: k = XK_Home; break;
2454 case NSPrevFunctionKey: k = XK_Prior; break;
2455 case NSNextFunctionKey: k = XK_Next; break;
2456 case NSBeginFunctionKey: k = XK_Begin; break;
2457 case NSEndFunctionKey: k = XK_End; break;
2458 case NSF1FunctionKey: k = XK_F1; break;
2459 case NSF2FunctionKey: k = XK_F2; break;
2460 case NSF3FunctionKey: k = XK_F3; break;
2461 case NSF4FunctionKey: k = XK_F4; break;
2462 case NSF5FunctionKey: k = XK_F5; break;
2463 case NSF6FunctionKey: k = XK_F6; break;
2464 case NSF7FunctionKey: k = XK_F7; break;
2465 case NSF8FunctionKey: k = XK_F8; break;
2466 case NSF9FunctionKey: k = XK_F9; break;
2467 case NSF10FunctionKey: k = XK_F10; break;
2468 case NSF11FunctionKey: k = XK_F11; break;
2469 case NSF12FunctionKey: k = XK_F12; break;
2472 const char *ss = [ns cStringUsingEncoding:NSUTF8StringEncoding];
2473 k = (ss && *ss ? *ss : 0);
2479 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
2481 xe.xkey.keycode = k;
2482 xe.xkey.state = state;
2486 NSAssert1 (0, @"unknown X11 event type: %d", type);
2490 return [self sendEvent: &xe];
2494 - (void) mouseDown: (NSEvent *) e
2496 if (! [self convertEvent:e type:ButtonPress])
2497 [super mouseDown:e];
2500 - (void) mouseUp: (NSEvent *) e
2502 if (! [self convertEvent:e type:ButtonRelease])
2506 - (void) otherMouseDown: (NSEvent *) e
2508 if (! [self convertEvent:e type:ButtonPress])
2509 [super otherMouseDown:e];
2512 - (void) otherMouseUp: (NSEvent *) e
2514 if (! [self convertEvent:e type:ButtonRelease])
2515 [super otherMouseUp:e];
2518 - (void) mouseMoved: (NSEvent *) e
2520 if (! [self convertEvent:e type:MotionNotify])
2521 [super mouseMoved:e];
2524 - (void) mouseDragged: (NSEvent *) e
2526 if (! [self convertEvent:e type:MotionNotify])
2527 [super mouseDragged:e];
2530 - (void) otherMouseDragged: (NSEvent *) e
2532 if (! [self convertEvent:e type:MotionNotify])
2533 [super otherMouseDragged:e];
2536 - (void) scrollWheel: (NSEvent *) e
2538 if (! [self convertEvent:e type:ButtonPress])
2539 [super scrollWheel:e];
2542 - (void) keyDown: (NSEvent *) e
2544 if (! [self convertEvent:e type:KeyPress])
2548 - (void) keyUp: (NSEvent *) e
2550 if (! [self convertEvent:e type:KeyRelease])
2554 - (void) flagsChanged: (NSEvent *) e
2556 if (! [self convertEvent:e type:KeyPress])
2557 [super flagsChanged:e];
2561 - (NSOpenGLPixelFormat *) getGLPixelFormat
2563 NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
2565 NSOpenGLPixelFormatAttribute attrs[40];
2567 attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
2569 /* OpenGL's core profile removes a lot of the same stuff that was removed in
2570 OpenGL ES (e.g. glBegin, glDrawPixels), so it might be a possibility.
2572 opengl_core_p = True;
2573 if (opengl_core_p) {
2574 attrs[i++] = NSOpenGLPFAOpenGLProfile;
2575 attrs[i++] = NSOpenGLProfileVersion3_2Core;
2579 /* Eventually: multisampled pixmaps. May not be supported everywhere.
2580 if (multi_sample_p) {
2581 attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1;
2582 attrs[i++] = NSOpenGLPFASamples; attrs[i++] = 6;
2586 # ifdef JWXYZ_QUARTZ
2587 // Under Quartz, we're just blitting a texture.
2588 if (double_buffered_p)
2589 attrs[i++] = NSOpenGLPFADoubleBuffer;
2593 /* Under OpenGL, all sorts of drawing commands are being issued, and it might
2594 be a performance problem if this activity occurs on the front buffer.
2595 Also, some screenhacks expect OS X/iOS to always double-buffer.
2596 NSOpenGLPFABackingStore prevents flickering with screenhacks that
2597 don't redraw the entire screen every frame.
2599 attrs[i++] = NSOpenGLPFADoubleBuffer;
2600 attrs[i++] = NSOpenGLPFABackingStore;
2603 # pragma clang diagnostic push // "NSOpenGLPFAWindow deprecated in 10.9"
2604 # pragma clang diagnostic ignored "-Wdeprecated"
2605 attrs[i++] = NSOpenGLPFAWindow;
2606 # pragma clang diagnostic pop
2609 attrs[i++] = NSOpenGLPFAPixelBuffer;
2610 /* ...But not NSOpenGLPFAFullScreen, because that would be for
2611 [NSOpenGLContext setFullScreen].
2615 /* NSOpenGLPFAFullScreen would go here if initWithFrame's isPreview == NO.
2620 NSOpenGLPixelFormat *p = [[NSOpenGLPixelFormat alloc]
2621 initWithAttributes:attrs];
2626 #else // HAVE_IPHONE
2629 - (void) stopAndClose
2631 [self stopAndClose:NO];
2635 - (void) stopAndClose:(Bool)relaunch_p
2637 if ([self isAnimating])
2638 [self stopAnimation];
2640 /* Need to make the SaverListController be the firstResponder again
2641 so that it can continue to receive its own shake events. I
2642 suppose that this abstraction-breakage means that I'm adding
2643 XScreenSaverView to the UINavigationController wrong...
2645 // UIViewController *v = [[self window] rootViewController];
2646 // if ([v isKindOfClass: [UINavigationController class]]) {
2647 // UINavigationController *n = (UINavigationController *) v;
2648 // [[n topViewController] becomeFirstResponder];
2650 [self resignFirstResponder];
2652 if (relaunch_p) { // Fake a shake on the SaverListController.
2653 [_delegate didShake:self];
2654 } else { // Not launching another, animate our return to the list.
2655 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
2656 NSLog (@"fading back to saver list");
2658 [_delegate wantsFadeOut:self];
2663 // The cycle timer behaves just like a shake event.
2666 [self stopAndClose:YES];
2670 /* We distinguish between taps and drags.
2672 - Drags/pans (down, motion, up) are sent to the saver to handle.
2673 - Single-taps are sent to the saver to handle.
2674 - Double-taps are sent to the saver as a "Space" keypress.
2675 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
2676 - All taps expose the momentary "Close" button.
2679 - (void)initGestures
2681 UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
2683 action:@selector(handleDoubleTap)];
2684 dtap.numberOfTapsRequired = 2;
2686 dtap.numberOfTouchesRequired = 1;
2687 # endif // !HAVE_TVOS
2689 UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
2691 action:@selector(handleTap:)];
2692 stap.numberOfTapsRequired = 1;
2694 stap.numberOfTouchesRequired = 1;
2695 # endif // !HAVE_TVOS
2697 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
2699 action:@selector(handlePan:)];
2701 pan.maximumNumberOfTouches = 1;
2702 pan.minimumNumberOfTouches = 1;
2703 # endif // !HAVE_TVOS
2705 // I couldn't get Swipe to work, but using a second Pan recognizer works.
2706 UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
2708 action:@selector(handlePan2:)];
2710 pan2.maximumNumberOfTouches = 2;
2711 pan2.minimumNumberOfTouches = 2;
2712 # endif // !HAVE_TVOS
2714 // Also handle long-touch, and treat that the same as Pan.
2715 // Without this, panning doesn't start until there's motion, so the trick
2716 // of holding down your finger to freeze the scene doesn't work.
2718 UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
2720 action:@selector(handleLongPress:)];
2721 hold.numberOfTapsRequired = 0;
2723 hold.numberOfTouchesRequired = 1;
2724 # endif // !HAVE_TVOS
2725 hold.minimumPressDuration = 0.25; /* 1/4th second */
2728 // Two finger pinch to zoom in on the view.
2729 UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]
2731 action:@selector(handlePinch:)];
2732 # endif // !HAVE_TVOS
2734 [stap requireGestureRecognizerToFail: dtap];
2735 [stap requireGestureRecognizerToFail: hold];
2736 [dtap requireGestureRecognizerToFail: hold];
2737 [pan requireGestureRecognizerToFail: hold];
2739 [pan2 requireGestureRecognizerToFail: pinch];
2741 [self setMultipleTouchEnabled:YES];
2742 # endif // !HAVE_TVOS
2744 // As of Oct 2021 (macOS 11.6, iOS 15.1) minimumNumberOfTouches and
2745 // maximumNumberOfTouches are being ignored in gesture recognisers,
2746 // so the 'pan2' recognizer was firing for single-touch pans.
2747 // Instead, we now have the 'pan' do a horrible kludge, see below.
2749 [self addGestureRecognizer: dtap];
2750 [self addGestureRecognizer: stap];
2751 [self addGestureRecognizer: pan];
2752 //[self addGestureRecognizer: pan2];
2753 [self addGestureRecognizer: hold];
2755 [self addGestureRecognizer: pinch];
2756 # endif // !HAVE_TVOS
2765 # endif // !HAVE_TVOS
2769 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
2770 convert it to what X11 and OpenGL expect.
2772 Getting this crap right is tricky, given the confusion of the various
2773 scale factors, so here's a checklist that I think covers all of the X11
2774 and OpenGL cases. For each of these: rotate to all 4 orientations;
2775 ensure the mouse tracks properly to all 4 corners.
2777 Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
2779 Test hacks must cover:
2780 X11 ignoreRotation = true
2781 X11 ignoreRotation = false
2782 OpenGL (rotation is handled manually, so they never ignoreRotation)
2784 Test devices must cover:
2785 contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
2786 contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
2787 contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
2789 iPad 2: 768x1024 / 1 = 768x1024
2790 iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
2791 iPhone 4s: 640x960 / 2 = 320x480
2792 iPhone 5: 640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
2793 iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
2796 iPad2 iPadAir iPhone4s iPhone5 iPhone6+
2797 Attraction X yes - - - - Y
2798 Fireworkx X no - - - - Y
2799 Carousel GL yes - - - - Y
2800 Voronoi GL no - - - - -
2802 - (void) convertMouse:(CGPoint *)p
2804 CGFloat xx = p->x, yy = p->y;
2806 # if 0 // TARGET_IPHONE_SIMULATOR
2808 XWindowAttributes xgwa;
2809 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2810 NSLog (@"TOUCH %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2812 xgwa.width, xgwa.height,
2813 [self contentScaleFactor],
2814 [self hackedContentScaleFactor],
2815 [self rotateTouches], [self ignoreRotation]);
2817 # endif // TARGET_IPHONE_SIMULATOR
2819 if ([self rotateTouches]) {
2821 // The XScreenSaverGLView case:
2822 // The X11 window is rotated, as is the framebuffer.
2823 // The device coordinates match the framebuffer dimensions,
2824 // but might have axes swapped... and we need to swap them
2827 int w = [self frame].size.width;
2828 int h = [self frame].size.height;
2829 GLfloat xr = (GLfloat) xx / w;
2830 GLfloat yr = (GLfloat) yy / h;
2832 int o = (int) current_device_rotation();
2834 case -90: case 270: swap = xr; xr = 1-yr; yr = swap; break;
2835 case 90: case -270: swap = xr; xr = yr; yr = 1-swap; break;
2836 case 180: case -180: xr = 1-xr; yr = 1-yr; break;
2842 } else if ([self ignoreRotation]) {
2844 // The X11 case, where the hack has opted not to rotate:
2845 // The X11 window is unrotated, but the framebuffer is rotated.
2846 // The device coordinates match the framebuffer, so they need to
2847 // be de-rotated to match the X11 window.
2849 int w = [self frame].size.width;
2850 int h = [self frame].size.height;
2852 int o = (int) current_device_rotation();
2854 case -90: case 270: swap = xx; xx = h-yy; yy = swap; break;
2855 case 90: case -270: swap = xx; xx = yy; yy = w-swap; break;
2856 case 180: case -180: xx = w-xx; yy = h-yy; break;
2861 double s = [self hackedContentScaleFactor];
2865 # if 0 // TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
2867 XWindowAttributes xgwa;
2868 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2869 NSLog (@"touch %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2871 xgwa.width, xgwa.height,
2872 [self contentScaleFactor],
2873 [self hackedContentScaleFactor],
2874 [self rotateTouches], [self ignoreRotation]);
2875 if (p->x < 0 || p->y < 0 || p->x > xgwa.width || p->y > xgwa.height)
2878 # endif // TARGET_IPHONE_SIMULATOR
2882 - (void) handleTap:(UIGestureRecognizer *)sender
2888 memset (&xe, 0, sizeof(xe));
2890 [self showCloseButton];
2892 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2893 [self convertMouse:&p];
2894 NSAssert (xwindow->type == WINDOW, @"not a window");
2895 xwindow->window.last_mouse_x = p.x;
2896 xwindow->window.last_mouse_y = p.y;
2898 xe.xany.type = ButtonPress;
2899 xe.xbutton.button = 1;
2903 # ifndef __OPTIMIZE__
2904 NSLog (@"tap ButtonPress %d %d", xe.xbutton.x, xe.xbutton.y);
2907 if (! [self sendEvent: &xe])
2910 xe.xany.type = ButtonRelease;
2911 xe.xbutton.button = 1;
2915 [self sendEvent: &xe];
2919 /* Double click sends Space KeyPress.
2921 - (void) handleDoubleTap
2923 if (!xsft->event_cb || !xwindow) return;
2925 [self showCloseButton];
2927 # ifndef __OPTIMIZE__
2928 NSLog (@"double-tap KeyPress Space");
2932 memset (&xe, 0, sizeof(xe));
2933 xe.xkey.keycode = ' ';
2934 xe.xany.type = KeyPress;
2935 BOOL ok1 = [self sendEvent: &xe];
2936 xe.xany.type = KeyRelease;
2937 BOOL ok2 = [self sendEvent: &xe];
2943 /* Drag with one finger down: send MotionNotify.
2945 - (void) handlePan:(UIGestureRecognizer *)sender
2947 if (!xsft->event_cb || !xwindow) return;
2949 // As of Oct 2021 (macOS 11.6, iOS 15.1) minimumNumberOfTouches and
2950 // maximumNumberOfTouches are being ignored in gesture recognisers.
2951 // Thus, this bullshit. If we get a multi-touch in this recogniser
2952 // (which we should not, as it set max and min touches to 1) then
2953 // do the double-touch swipe handler instead. the static state is
2954 // needed because 'StateEnded' is called with numberOfTouches == 0.
2956 static BOOL pan2_p = FALSE;
2957 if (sender.state == UIGestureRecognizerStateBegan) {
2959 if (sender.numberOfTouches == 2)
2964 [self handlePan2: (UIPanGestureRecognizer *) sender];
2969 if (sender.numberOfTouches > 1)
2972 [self showCloseButton];
2975 memset (&xe, 0, sizeof(xe));
2977 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2978 [self convertMouse:&p];
2979 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
2980 xwindow->window.last_mouse_x = p.x;
2981 xwindow->window.last_mouse_y = p.y;
2983 switch (sender.state) {
2984 case UIGestureRecognizerStateBegan:
2985 xe.xany.type = ButtonPress;
2986 xe.xbutton.button = 1;
2991 case UIGestureRecognizerStateEnded:
2992 xe.xany.type = ButtonRelease;
2993 xe.xbutton.button = 1;
2998 case UIGestureRecognizerStateChanged:
2999 xe.xany.type = MotionNotify;
3008 # ifndef __OPTIMIZE__
3009 if (xe.xany.type != MotionNotify)
3010 NSLog (@"pan (%lu) %s %d %d",
3011 sender.numberOfTouches,
3012 (xe.xany.type == ButtonPress ? "ButtonPress" :
3013 xe.xany.type == ButtonRelease ? "ButtonRelease" :
3014 xe.xany.type == MotionNotify ? "MotionNotify" : "???"),
3015 (int) p.x, (int) p.y);
3018 BOOL ok = [self sendEvent: &xe];
3019 if (!ok && xe.xany.type == ButtonRelease)
3024 /* Hold one finger down: assume we're about to start dragging.
3025 Treat the same as Pan.
3027 - (void) handleLongPress:(UIGestureRecognizer *)sender
3029 # ifndef __OPTIMIZE__
3030 NSLog (@"long-press");
3032 [self handlePan:sender];
3037 /* Drag with 2 fingers down: send arrow keys.
3039 - (void) handlePan2:(UIPanGestureRecognizer *)sender
3041 if (!xsft->event_cb || !xwindow) return;
3043 [self showCloseButton];
3045 if (sender.state != UIGestureRecognizerStateEnded)
3049 memset (&xe, 0, sizeof(xe));
3051 CGPoint p = [sender translationInView:self]; // this is in points, not pixels
3052 [self convertMouse:&p];
3054 if (fabs(p.x) > fabs(p.y))
3055 xe.xkey.keycode = (p.x > 0 ? XK_Right : XK_Left);
3057 xe.xkey.keycode = (p.y > 0 ? XK_Down : XK_Up);
3059 # ifndef __OPTIMIZE__
3060 NSLog (@"pan2 (%lu) KeyPress %s",
3061 sender.numberOfTouches,
3062 (xe.xkey.keycode == XK_Right ? "Right" :
3063 xe.xkey.keycode == XK_Left ? "Left" :
3064 xe.xkey.keycode == XK_Up ? "Up" : "Down"));
3067 xe.xany.type = KeyPress;
3068 BOOL ok1 = [self sendEvent: &xe];
3069 xe.xany.type = KeyRelease;
3070 BOOL ok2 = [self sendEvent: &xe];
3077 /* Pinch with 2 fingers: zoom in around the center of the fingers.
3079 - (void) handlePinch:(UIPinchGestureRecognizer *)sender
3081 if (!xsft->event_cb || !xwindow) return;
3083 [self showCloseButton];
3085 if (sender.state == UIGestureRecognizerStateBegan)
3086 pinch_transform = self.transform; // Save the base transform
3088 switch (sender.state) {
3089 case UIGestureRecognizerStateBegan:
3090 # ifndef __OPTIMIZE__
3091 NSLog (@"Pinch start");
3093 case UIGestureRecognizerStateChanged:
3095 double scale = sender.scale;
3100 self.transform = CGAffineTransformScale (pinch_transform, scale, scale);
3102 CGPoint p = [sender locationInView: self];
3103 p.x /= self.layer.bounds.size.width;
3104 p.y /= self.layer.bounds.size.height;
3106 CGPoint np = CGPointMake (self.bounds.size.width * p.x,
3107 self.bounds.size.height * p.y);
3108 CGPoint op = CGPointMake (self.bounds.size.width *
3109 self.layer.anchorPoint.x,
3110 self.bounds.size.height *
3111 self.layer.anchorPoint.y);
3112 np = CGPointApplyAffineTransform (np, self.transform);
3113 op = CGPointApplyAffineTransform (op, self.transform);
3115 CGPoint pos = self.layer.position;
3120 self.layer.position = pos;
3121 self.layer.anchorPoint = p;
3127 case UIGestureRecognizerStateEnded:
3129 // When released, snap back to the default zoom (but animate it).
3131 # ifndef __OPTIMIZE__
3132 NSLog (@"Pinch end");
3135 CABasicAnimation *a1 = [CABasicAnimation
3136 animationWithKeyPath:@"position.x"];
3137 a1.fromValue = [NSNumber numberWithFloat: self.layer.position.x];
3138 a1.toValue = [NSNumber numberWithFloat: self.bounds.size.width / 2];
3140 CABasicAnimation *a2 = [CABasicAnimation
3141 animationWithKeyPath:@"position.y"];
3142 a2.fromValue = [NSNumber numberWithFloat: self.layer.position.y];
3143 a2.toValue = [NSNumber numberWithFloat: self.bounds.size.height / 2];
3145 CABasicAnimation *a3 = [CABasicAnimation
3146 animationWithKeyPath:@"anchorPoint.x"];
3147 a3.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.x];
3148 a3.toValue = [NSNumber numberWithFloat: 0.5];
3150 CABasicAnimation *a4 = [CABasicAnimation
3151 animationWithKeyPath:@"anchorPoint.y"];
3152 a4.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.y];
3153 a4.toValue = [NSNumber numberWithFloat: 0.5];
3155 CABasicAnimation *a5 = [CABasicAnimation
3156 animationWithKeyPath:@"transform.scale"];
3157 a5.fromValue = [NSNumber numberWithFloat: sender.scale];
3158 a5.toValue = [NSNumber numberWithFloat: 1.0];
3160 CAAnimationGroup *group = [CAAnimationGroup animation];
3161 group.duration = 0.3;
3162 group.repeatCount = 1;
3163 group.autoreverses = NO;
3164 group.animations = @[ a1, a2, a3, a4, a5 ];
3165 group.timingFunction = [CAMediaTimingFunction
3167 kCAMediaTimingFunctionEaseIn];
3168 [self.layer addAnimation:group forKey:@"unpinch"];
3170 self.transform = pinch_transform;
3171 self.layer.anchorPoint = CGPointMake (0.5, 0.5);
3172 self.layer.position = CGPointMake (self.bounds.size.width / 2,
3173 self.bounds.size.height / 2);
3180 # endif // !HAVE_TVOS
3183 /* We need this to respond to "shake" gestures
3185 - (BOOL)canBecomeFirstResponder
3190 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
3195 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
3199 /* Shake means exit and launch a new saver.
3201 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
3203 [self stopAndClose:YES];
3207 - (void) showCloseButton
3209 int width = self.bounds.size.width;
3210 double scale = width > 800 ? 2 : 1; // iPad
3211 double iw = 24 * scale;
3213 double off = 4 * scale;
3216 closeBox = [[UIView alloc]
3217 initWithFrame:CGRectMake(0, 0, width, ih + off)];
3218 closeBox.backgroundColor = [UIColor clearColor];
3219 closeBox.autoresizingMask =
3220 UIViewAutoresizingFlexibleBottomMargin |
3221 UIViewAutoresizingFlexibleWidth;
3223 // Add the buttons to the bar
3224 UIImage *img1 = [UIImage imageNamed:@"stop"];
3225 UIImage *img2 = [UIImage imageNamed:@"settings"];
3227 UIButton *button = [[UIButton alloc] init];
3228 [button setFrame: CGRectMake(off, off, iw, ih)];
3229 [button setBackgroundImage:img1 forState:UIControlStateNormal];
3230 [button addTarget:self
3231 action:@selector(stopAndClose)
3232 forControlEvents:UIControlEventTouchUpInside];
3233 [closeBox addSubview:button];
3236 button = [[UIButton alloc] init];
3237 [button setFrame: CGRectMake(width - iw - off, off, iw, ih)];
3238 [button setBackgroundImage:img2 forState:UIControlStateNormal];
3239 [button addTarget:self
3240 action:@selector(stopAndOpenSettings)
3241 forControlEvents:UIControlEventTouchUpInside];
3242 button.autoresizingMask =
3243 UIViewAutoresizingFlexibleBottomMargin |
3244 UIViewAutoresizingFlexibleLeftMargin;
3245 [closeBox addSubview:button];
3248 [self addSubview:closeBox];
3251 // Don't hide the buttons under the iPhone X bezel.
3252 UIEdgeInsets is = { 0, };
3253 if ([self respondsToSelector:@selector(safeAreaInsets)]) {
3254 # pragma clang diagnostic push // "only available on iOS 11.0 or newer"
3255 # pragma clang diagnostic ignored "-Wunguarded-availability-new"
3256 is = [self safeAreaInsets];
3257 # pragma clang diagnostic pop
3258 [closeBox setFrame:CGRectMake(is.left, is.top,
3259 self.bounds.size.width - is.right - is.left,
3263 if (closeBox.layer.opacity <= 0) { // Fade in
3265 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
3266 anim.duration = 0.2;
3267 anim.repeatCount = 1;
3268 anim.autoreverses = NO;
3269 anim.fromValue = [NSNumber numberWithFloat:0.0];
3270 anim.toValue = [NSNumber numberWithFloat:1.0];
3271 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
3272 closeBox.layer.opacity = 1;
3275 // Fade out N seconds from now.
3277 [closeBoxTimer invalidate];
3278 closeBoxTimer = [NSTimer scheduledTimerWithTimeInterval: 3
3280 selector:@selector(closeBoxOff)
3288 if (closeBoxTimer) {
3289 [closeBoxTimer invalidate];
3295 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
3296 anim.duration = 0.2;
3297 anim.repeatCount = 1;
3298 anim.autoreverses = NO;
3299 anim.fromValue = [NSNumber numberWithFloat: 1];
3300 anim.toValue = [NSNumber numberWithFloat: 0];
3301 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
3302 closeBox.layer.opacity = 0;
3306 - (void) stopAndOpenSettings
3308 if ([self isAnimating])
3309 [self stopAnimation];
3310 [self resignFirstResponder];
3311 [_delegate wantsFadeOut:self];
3312 [_delegate openPreferences: saver_title];
3316 - (void)setScreenLocked:(BOOL)locked
3318 if (screenLocked == locked) return;
3319 screenLocked = locked;
3321 if ([self isAnimating])
3322 [self stopAnimation];
3324 if (! [self isAnimating])
3325 [self startAnimation];
3329 - (NSDictionary *)getGLProperties
3331 return [NSDictionary dictionaryWithObjectsAndKeys:
3332 kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
3334 /* This could be disabled if we knew the screen would be redrawn
3335 entirely for every frame.
3337 [NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
3342 - (void)addExtraRenderbuffers:(CGSize)size
3344 // No extra renderbuffers are needed for 2D screenhacks.
3348 - (NSString *)getCAGravity
3350 return kCAGravityCenter; // Looks better in e.g. Compass.
3351 // return kCAGravityBottomLeft;
3354 #endif // HAVE_IPHONE
3357 # ifndef HAVE_IPHONE
3359 /* Hooooooboy, is checking for updates a mess!
3361 The various screen savers, via XScreenSaverConfigSheet, normally store
3362 their preferences into NSUserDefaultsController and ScreenSaverDefaults,
3363 which (when running under legacyScreenSaver, which is sandboxed) writes
3364 those preferences into:
3366 Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/\
3367 Library/Preferences/ByHost/org.jwz.xscreensaver.$NAME.$UUID.plist
3369 The exception to this is the two global preferences controlling whether
3370 and how often we should check for updates. Those two preferences,
3371 "SUAutomaticallyUpdate" and "SUScheduledCheckInterval", are instead
3372 written into NSUserDefaultsController / GlobalDefaults, which means
3375 Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/\
3376 Library/Preferences/org.jwz.xscreensaver.XScreenSaverUpdater.plist
3378 However, XScreenSaverUpdater.app, which is not sandboxed, expects to
3379 read those preferences from:
3381 Library/Preferences/org.jwz.xscreensaver.XScreenSaverUpdater.plist
3383 which cannot be written by a sandboxed .saver bundle.
3385 Also a sandboxed app cannot pass command-line args to a launched
3388 How we resolve this is by moving the "when to check" logic into
3389 XScreenSaverView. We examine the sandboxed versions of the Sparkle
3390 preferences and use those to decide when and whether to launch
3391 XScreenSaverUpdater.app. And we arrange for XScreenSaverUpdater.app
3392 to always check if it has been launched, by having these settings as
3395 SUAutomaticallyUpdate: yes
3396 SUScheduledCheckInterval: daily
3401 // Returns the full pathname to the Sparkle updater app.
3403 - (NSString *) updaterPath
3405 NSString *updater = @"XScreenSaverUpdater.app";
3407 // There may be multiple copies of the updater: e.g., one in /Applications
3408 // and one in the mounted installer DMG! It's important that we run the
3409 // one from the disk and not the DMG, so search for the right one.
3411 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
3412 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
3414 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
3415 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
3416 @"/Library/Screen Savers",
3417 @"/System/Library/Screen Savers",
3419 @"/Applications/Utilities"];
3420 NSString *app_path = nil;
3421 for (NSString *dir in search) {
3422 NSString *p = [dir stringByAppendingPathComponent:updater];
3423 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
3430 app_path = [workspace fullPathForApplication:updater];
3432 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
3433 app_path = 0; // The DMG version will not do.
3439 // Upon successful launch of the updater, record the date.
3441 - (void) updaterLaunched
3443 NSUserDefaultsController *def = [prefsReader globalDefaultsController];
3444 NSAssert (def, @"no globalDefaultsController");
3446 // SULastCheckTime = "2023-10-09 17:01:59 +0000";
3448 NSDateFormatter *f = [[NSDateFormatter alloc] init];
3449 [f setDateFormat:@"yyyy-MM-dd HH:mm:ss +0000"];
3450 [f setTimeZone: [NSTimeZone timeZoneForSecondsFromGMT: 0]];
3451 NSString *date = [f stringFromDate: [NSDate date]];
3453 NSString *old = [[def defaults] objectForKey: @SULastCheckTimeKey];
3454 NSLog (@"%@: \"%@\" => \"%@\"", @SULastCheckTimeKey, old, date);
3456 [[def defaults] setObject: date forKey: @SULastCheckTimeKey];
3457 [def commitEditing];
3462 - (void) checkForUpdates
3464 if (! get_boolean_resource (xdpy,
3465 SUSUEnableAutomaticChecksKey,
3466 SUSUEnableAutomaticChecksKey)) {
3467 NSLog (@"update checks disbled");
3471 int interval = get_integer_resource (xdpy,
3472 SUScheduledCheckIntervalKey,
3473 SUScheduledCheckIntervalKey);
3475 interval = 60 * 60 * 24;
3477 const char *last_check = get_string_resource (xdpy,
3479 SULastCheckTimeKey);
3480 if (!last_check || !*last_check) {
3481 NSLog (@"never checked for updates (interval %d days)",
3482 (int) (interval / (60 * 60 * 24)));
3484 NSDateFormatter *f = [[NSDateFormatter alloc] init];
3485 [f setDateFormat:@"yyyy-MM-dd HH:mm:ss ZZZZZ"];
3486 [f setTimeZone: [NSTimeZone timeZoneForSecondsFromGMT: 0]];
3487 NSDate *last_check2 =
3488 [f dateFromString: [NSString stringWithCString: last_check
3489 encoding: NSUTF8StringEncoding]];
3490 NSTimeInterval elapsed = -[last_check2 timeIntervalSinceNow];
3491 if (elapsed < interval) {
3492 NSLog (@"last checked for updates %d days ago, skipping check"
3493 " (interval %d days)",
3494 (int) (elapsed / (60 * 60 * 24)),
3495 (int) (interval / (60 * 60 * 24)));
3498 NSLog (@"last checked for updates %d days ago (interval %d days)",
3499 (int) (elapsed / (60 * 60 * 24)),
3500 (int) (interval / (60 * 60 * 24)));
3504 NSString *app_path = [self updaterPath];
3507 NSLog(@"Unable to find XScreenSaverUpdater.app");
3511 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
3513 # if 1 // Deprecated as of macOS 11.0:
3515 if ([workspace launchApplicationAtURL: [NSURL fileURLWithPath:app_path]
3516 options: (NSWorkspaceLaunchWithoutAddingToRecents |
3517 NSWorkspaceLaunchWithoutActivation |
3518 NSWorkspaceLaunchAndHide)
3519 configuration: [NSMutableDictionary dictionary]
3521 NSLog (@"Launched %@", app_path);
3522 [self updaterLaunched];
3524 NSLog (@"Unable to launch %@: %@", app_path, err);
3526 # else // Available in macOS 10.15 or newer:
3527 NSWorkspaceOpenConfiguration *conf =
3528 [NSWorkspaceOpenConfiguration configuration];
3529 conf.activates = NO;
3530 conf.addsToRecentItems = NO;
3531 conf.allowsRunningApplicationSubstitution = YES;
3532 conf.createsNewApplicationInstance = NO;
3534 conf.hidesOthers = NO;
3535 conf.promptsUserIfNeeded = YES;
3536 [workspace openApplicationAtURL: [NSURL fileURLWithPath:app_path]
3538 completionHandler: ^(NSRunningApplication *app, NSError *err) {
3540 NSLog(@"Unable to launch %@: %@", app, err);
3542 NSLog(@"Launched %@", app);
3543 [self updaterLaunched];
3549 # endif // !HAVE_IPHONE
3554 /* Utility functions...
3557 static PrefsReader *
3558 get_prefsReader (Display *dpy)
3560 if (! dpy) return 0;
3561 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
3562 if (!view) return 0;
3563 return [view prefsReader];
3568 get_string_resource (Display *dpy, char *name, char *class)
3570 return [get_prefsReader(dpy) getStringResource:name];
3574 get_boolean_resource (Display *dpy, char *name, char *class)
3576 return [get_prefsReader(dpy) getBooleanResource:name];
3580 get_integer_resource (Display *dpy, char *name, char *class)
3582 return [get_prefsReader(dpy) getIntegerResource:name];
3586 get_float_resource (Display *dpy, char *name, char *class)
3588 return [get_prefsReader(dpy) getFloatResource:name];