1 /* xscreensaver, Copyright (c) 2006-2017 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>
21 #import "XScreenSaverView.h"
22 #import "XScreenSaverConfigSheet.h"
24 #import "screenhackI.h"
27 #import "jwxyz-cocoa.h"
28 #import "jwxyz-timers.h"
31 // XScreenSaverView.m speaks OpenGL ES just fine, but enableBackbuffer does
32 // need (jwzgles_)gluCheckExtension.
35 # import <OpenGL/glu.h>
38 /* Garbage collection only exists if we are being compiled against the
39 10.6 SDK or newer, not if we are building against the 10.4 SDK.
41 #ifndef MAC_OS_X_VERSION_10_6
42 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
44 #ifndef MAC_OS_X_VERSION_10_12
45 # define MAC_OS_X_VERSION_10_12 101200
47 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 && \
48 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12)
49 /* 10.6 SDK or later, and earlier than 10.12 SDK */
50 # import <objc/objc-auto.h>
51 # define DO_GC_HACKERY
54 /* Duplicated in xlockmoreI.h and XScreenSaverGLView.m. */
55 extern void clear_gl_error (void);
56 extern void check_gl_error (const char *type);
58 extern struct xscreensaver_function_table *xscreensaver_function_table;
60 /* Global variables used by the screen savers
63 const char *progclass;
69 # define NSSizeToCGSize(x) (x)
71 extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
73 /* Stub definition of the superclass, for iPhone.
75 @implementation ScreenSaverView
77 NSTimeInterval anim_interval;
82 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
83 self = [super initWithFrame:frame];
85 anim_interval = 1.0/30;
88 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
89 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
90 - (BOOL)hasConfigureSheet { return NO; }
91 - (NSWindow *)configureSheet { return nil; }
92 - (NSView *)configureView { return nil; }
93 - (BOOL)isPreview { return NO; }
94 - (BOOL)isAnimating { return animating_p; }
95 - (void)animateOneFrame { }
97 - (void)startAnimation {
98 if (animating_p) return;
100 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
102 selector:@selector(animateOneFrame)
107 - (void)stopAnimation {
109 [anim_timer invalidate];
116 # endif // !USE_IPHONE
120 @interface XScreenSaverView (Private)
121 - (void) stopAndClose;
122 - (void) stopAndClose:(Bool)relaunch;
125 @implementation XScreenSaverView
127 // Given a lower-cased saver name, returns the function table for it.
128 // If no name, guess the name from the class's bundle name.
130 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
132 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
133 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
135 NSString *path = [nsb bundlePath];
136 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
138 kCFURLPOSIXPathStyle,
140 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
142 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
143 // #### Analyze says "Potential leak of an object stored into cfb"
146 name = [[path lastPathComponent] stringByDeletingPathExtension];
148 name = [[name lowercaseString]
149 stringByReplacingOccurrencesOfString:@" "
153 // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
154 // I'm guessing that symbol-stripping is mandatory. Fuck.
155 NSString *table_name = [name stringByAppendingString:
156 @"_xscreensaver_function_table"];
157 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
161 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
164 // Depends on the auto-generated "ios-function-table.m" being up to date.
165 if (! function_tables)
166 function_tables = [make_function_table_dict() retain];
167 NSValue *v = [function_tables objectForKey: name];
168 void *addr = v ? [v pointerValue] : 0;
169 # endif // USE_IPHONE
171 return (struct xscreensaver_function_table *) addr;
175 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
176 // to $PATH for the benefit of savers that include helper shell scripts.
178 - (void) setShellPath
180 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
181 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
183 NSString *nsdir = [nsb resourcePath];
184 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
185 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
186 const char *opath = getenv ("PATH");
187 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
188 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 2);
191 strcat (npath, opath);
192 if (setenv ("PATH", npath, 1)) {
194 NSAssert1 (0, @"setenv \"PATH=%s\" failed", npath);
201 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
202 // (e.g., "xscreensaver-text") know how to look up resources.
204 - (void) setResourcesEnv:(NSString *) name
206 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
207 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
209 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
210 if (setenv ("XSCREENSAVER_CLASSPATH", s, 1)) {
212 NSAssert1 (0, @"setenv \"XSCREENSAVER_CLASSPATH=%s\" failed", s);
217 - (void) loadCustomFonts
220 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
221 NSMutableArray *fonts = [NSMutableArray arrayWithCapacity:20];
222 for (NSString *ext in @[@"ttf", @"otf"]) {
223 [fonts addObjectsFromArray: [nsb pathsForResourcesOfType:ext
226 for (NSString *font in fonts) {
227 CFURLRef url = (CFURLRef) [NSURL fileURLWithPath: font];
229 if (! CTFontManagerRegisterFontsForURL (url, kCTFontManagerScopeProcess,
231 // Just ignore errors:
232 // "The file has already been registered in the specified scope."
233 // NSLog (@"loading font: %@ %@", url, err);
236 # endif // !USE_IPHONE
241 add_default_options (const XrmOptionDescRec *opts,
242 const char * const *defs,
243 XrmOptionDescRec **opts_ret,
244 const char ***defs_ret)
246 /* These aren't "real" command-line options (there are no actual command-line
247 options in the Cocoa version); but this is the somewhat kludgey way that
248 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
249 ../hacks/config/\*.xml files communicate with the preferences database.
251 static const XrmOptionDescRec default_options [] = {
252 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
253 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
254 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
255 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
256 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
257 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
258 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
259 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
260 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
261 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
262 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
263 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
264 { "-foreground", ".foreground", XrmoptionSepArg, 0 },
265 { "-fg", ".foreground", XrmoptionSepArg, 0 },
266 { "-background", ".background", XrmoptionSepArg, 0 },
267 { "-bg", ".background", XrmoptionSepArg, 0 },
270 // <xscreensaver-updater />
271 { "-" SUSUEnableAutomaticChecksKey,
272 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" },
273 { "-no-" SUSUEnableAutomaticChecksKey,
274 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
275 { "-" SUAutomaticallyUpdateKey,
276 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" },
277 { "-no-" SUAutomaticallyUpdateKey,
278 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
279 { "-" SUSendProfileInfoKey,
280 "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
281 { "-no-" SUSendProfileInfoKey,
282 "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
283 { "-" SUScheduledCheckIntervalKey,
284 "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
285 # endif // !USE_IPHONE
289 static const char *default_defaults [] = {
291 # if defined(USE_IPHONE) && !defined(__OPTIMIZE__)
296 ".doubleBuffer: True",
297 ".multiSample: False",
305 ".textURL: https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
307 ".grabDesktopImages: yes",
309 ".chooseRandomImages: no",
311 ".chooseRandomImages: yes",
313 ".imageDirectory: ~/Pictures",
315 ".texFontCacheSize: 30",
319 # define STR(S) STR1(S)
320 # define __objc_yes Yes
321 # define __objc_no No
322 "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
323 "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef),
324 "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef),
325 "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef),
330 # endif // USE_IPHONE
335 for (i = 0; default_options[i].option; i++)
337 for (i = 0; opts[i].option; i++)
340 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
341 calloc (count + 1, sizeof (*opts2));
345 while (default_options[j].option) {
346 opts2[i] = default_options[j];
350 while (opts[j].option) {
361 for (i = 0; default_defaults[i]; i++)
363 for (i = 0; defs[i]; i++)
366 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
370 while (default_defaults[j]) {
371 defs2[i] = default_defaults[j];
384 - (id) initWithFrame:(NSRect)frame
385 saverName:(NSString *)saverName
386 isPreview:(BOOL)isPreview
388 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
391 xsft = [self findFunctionTable: saverName];
401 xsft->setup_cb (xsft, xsft->setup_arg);
403 /* The plist files for these preferences show up in
404 $HOME/Library/Preferences/ByHost/ in a file named like
405 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
407 NSString *name = [NSString stringWithCString:xsft->progclass
408 encoding:NSISOLatin1StringEncoding];
409 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
410 [self setResourcesEnv:name];
411 [self loadCustomFonts];
413 XrmOptionDescRec *opts = 0;
414 const char **defs = 0;
415 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
416 prefsReader = [[PrefsReader alloc]
417 initWithName:name xrmKeys:opts defaults:defs];
419 // free (opts); // bah, we need these! #### leak!
420 xsft->options = opts;
422 progname = progclass = xsft->progclass;
426 # if !defined USE_IPHONE && defined JWXYZ_QUARTZ
427 // When the view fills the screen and double buffering is enabled, OS X will
428 // use page flipping for a minor CPU/FPS boost. In windowed mode, double
429 // buffering reduces the frame rate to 1/2 the screen's refresh rate.
430 double_buffered_p = !isPreview;
436 // So we can tell when we're docked.
437 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
439 [self setBackgroundColor:[NSColor blackColor]];
440 # endif // USE_IPHONE
443 // Colorspaces and CGContexts only happen with non-GL hacks.
444 colorspace = CGColorSpaceCreateDeviceRGB ();
454 return [CAEAGLLayer class];
459 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
461 return [self initWithFrame:frame saverName:0 isPreview:p];
467 if ([self isAnimating])
468 [self stopAnimation];
469 NSAssert(!xdata, @"xdata not yet freed");
470 NSAssert(!xdpy, @"xdpy not yet freed");
473 [[NSNotificationCenter defaultCenter] removeObserver:self];
476 # ifdef BACKBUFFER_OPENGL
479 # endif // !USE_IPHONE
481 // Releasing the OpenGL context should also free any OpenGL objects,
482 // including the backbuffer texture and frame/render/depthbuffers.
483 # endif // BACKBUFFER_OPENGL
485 # if defined JWXYZ_GL && defined USE_IPHONE
486 [ogl_ctx_pixmap release];
491 CGColorSpaceRelease (colorspace);
492 # endif // JWXYZ_QUARTZ
494 [prefsReader release];
502 - (PrefsReader *) prefsReader
509 - (void) lockFocus { }
510 - (void) unlockFocus { }
516 /* A few seconds after the saver launches, we store the "wasRunning"
517 preference. This is so that if the saver is crashing at startup,
518 we don't launch it again next time, getting stuck in a crash loop.
520 - (void) allSystemsGo: (NSTimer *) timer
522 NSAssert (timer == crash_timer, @"crash timer screwed up");
525 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
526 [prefs setBool:YES forKey:@"wasRunning"];
536 CGSize screen_size = self.bounds.size;
537 double s = self.contentScaleFactor;
538 screen_size.width *= s;
539 screen_size.height *= s;
542 GLuint *framebuffer = &xwindow->gl_framebuffer;
543 GLuint *renderbuffer = &xwindow->gl_renderbuffer;
544 xwindow->window.current_drawable = xwindow;
545 #elif defined JWXYZ_QUARTZ
546 GLuint *framebuffer = &gl_framebuffer;
547 GLuint *renderbuffer = &gl_renderbuffer;
548 #endif // JWXYZ_QUARTZ
550 if (*framebuffer) glDeleteFramebuffersOES (1, framebuffer);
551 if (*renderbuffer) glDeleteRenderbuffersOES (1, renderbuffer);
553 create_framebuffer (framebuffer, renderbuffer);
556 // glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES,
557 // (int)size.width, (int)size.height);
558 [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
559 fromDrawable:(CAEAGLLayer*)self.layer];
561 glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,
562 GL_RENDERBUFFER_OES, *renderbuffer);
564 [self addExtraRenderbuffers:screen_size];
566 check_framebuffer_status();
571 - (void) startAnimation
573 NSAssert(![self isAnimating], @"already animating");
574 NSAssert(!initted_p && !xdata, @"already initialized");
576 // See comment in render_x11() for why this value is important:
577 [self setAnimationTimeInterval: 1.0 / 240.0];
579 [super startAnimation];
580 /* We can't draw on the window from this method, so we actually do the
581 initialization of the screen saver (xsft->init_cb) in the first call
582 to animateOneFrame() instead.
587 [crash_timer invalidate];
589 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
590 [prefs removeObjectForKey:@"wasRunning"];
593 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
595 selector:@selector(allSystemsGo:)
599 # endif // USE_IPHONE
601 // Never automatically turn the screen off if we are docked,
602 // and an animation is running.
605 [UIApplication sharedApplication].idleTimerDisabled =
606 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
609 xwindow = (Window) calloc (1, sizeof(*xwindow));
610 xwindow->type = WINDOW;
611 xwindow->window.view = self;
612 CFRetain (xwindow->window.view); // needed for garbage collection?
614 #ifdef BACKBUFFER_OPENGL
615 CGSize new_backbuffer_size;
621 pixfmt = [self getGLPixelFormat];
624 NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
626 // Fun: On OS X 10.7, the second time an OpenGL context is created, after
627 // the preferences dialog is launched in SaverTester, the context only
628 // lasts until the first full GC. Then it turns black. Solution is to
629 // reuse the OpenGL context after this point.
630 // "Analyze" says that both pixfmt and ogl_ctx are leaked.
631 ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
634 // Sync refreshes to the vertical blanking interval
636 [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
637 // check_gl_error ("NSOpenGLCPSwapInterval"); // SEGV sometimes. Too early?
640 [ogl_ctx makeCurrentContext];
641 check_gl_error ("makeCurrentContext");
643 // NSOpenGLContext logs an 'invalid drawable' when this is called
644 // from initWithFrame.
645 [ogl_ctx setView:self];
647 // Get device pixels instead of points.
648 self.wantsBestResolutionOpenGLSurface = YES;
650 // This may not be necessary if there's FBO support.
652 xwindow->window.pixfmt = pixfmt;
653 CFRetain (xwindow->window.pixfmt);
654 xwindow->window.virtual_screen = [ogl_ctx currentVirtualScreen];
655 xwindow->window.current_drawable = xwindow;
656 NSAssert (ogl_ctx, @"no CGContext");
659 // Clear frame buffer ASAP, else there are bits left over from other apps.
660 glClearColor (0, 0, 0, 1);
661 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
663 // glXSwapBuffers (mi->dpy, mi->window);
666 // Enable multi-threading, if possible. This runs most OpenGL commands
667 // and GPU management on a second CPU.
669 # ifndef kCGLCEMPEngine
670 # define kCGLCEMPEngine 313 // Added in MacOS 10.4.8 + XCode 2.4.
672 CGLContextObj cctx = CGLGetCurrentContext();
673 CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
674 if (err != kCGLNoError) {
675 NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
679 new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
681 // Scale factor for desktop retina displays
682 double s = [self hackedContentScaleFactor];
683 new_backbuffer_size.width *= s;
684 new_backbuffer_size.height *= s;
688 CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
689 eagl_layer.opaque = TRUE;
690 eagl_layer.drawableProperties = [self getGLProperties];
692 // Without this, the GL frame buffer is half the screen resolution!
693 eagl_layer.contentsScale = [UIScreen mainScreen].scale;
695 ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
697 ogl_ctx_pixmap = [[EAGLContext alloc]
698 initWithAPI:kEAGLRenderingAPIOpenGLES1
699 sharegroup:ogl_ctx.sharegroup];
702 eagl_layer.contentsGravity = [self getCAGravity];
706 xwindow->window.ogl_ctx_pixmap = ogl_ctx_pixmap;
709 [EAGLContext setCurrentContext: ogl_ctx];
713 double s = [self hackedContentScaleFactor];
714 new_backbuffer_size = self.bounds.size;
715 new_backbuffer_size.width *= s;
716 new_backbuffer_size.height *= s;
718 # endif // USE_IPHONE
721 xwindow->ogl_ctx = ogl_ctx;
723 CFRetain (xwindow->ogl_ctx);
724 # endif // USE_IPHONE
727 check_gl_error ("startAnimation");
729 // NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
730 // glGetString (GL_RENDERER), glGetString (GL_VERSION));
732 [self enableBackbuffer:new_backbuffer_size];
734 #endif // BACKBUFFER_OPENGL
737 [self createBackbuffer:new_backbuffer_size];
740 static BOOL created_touchbar = NO;
742 if (!touchbar_view &&
743 //#### !self.isPreview &&
744 self.window.screen == [[NSScreen screens] objectAtIndex: 0] &&
747 // Figure out which NSScreen has the touchbar on it;
748 // find its bounds; create a saver there.
750 created_touchbar = YES;
751 NSScreen *tbs = [[NSScreen screens] lastObject]; // #### write me
752 NSRect rect = [tbs visibleFrame];
757 rect.size.width /= 4;
758 rect.size.height /= 4;
759 NSLog(@"## TB %.0f, %.0f %.0f x %.0f",
760 rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
762 touchbar_view = [[[self class] alloc]
764 saverName:[NSString stringWithCString:xsft->progclass
765 encoding:NSISOLatin1StringEncoding]
766 isPreview:self.isPreview];
767 [touchbar_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
769 touchbar_window = [[NSWindow alloc]
770 initWithContentRect:rect
771 styleMask: (NSTitledWindowMask|NSResizableWindowMask)
772 backing:NSBackingStoreBuffered
775 [touchbar_window setTitle: @"XScreenSaver Touchbar"];
776 [[touchbar_window contentView] addSubview: touchbar_view];
777 [touchbar_window makeKeyAndOrderFront:touchbar_window];
780 if (touchbar_view) [touchbar_view startAnimation];
781 # endif // USE_TOUCHBAR
784 - (void)stopAnimation
786 NSAssert([self isAnimating], @"not animating");
790 [self lockFocus]; // in case something tries to draw from here
791 [self prepareContext];
793 /* All of the xlockmore hacks need to have their release functions
794 called, or launching the same saver twice does not work. Also
795 webcollage-cocoa needs it in order to kill the inferior webcollage
796 processes (since the screen saver framework never generates a
800 xsft->free_cb (xdpy, xwindow, xdata);
803 jwxyz_quartz_free_display (xdpy);
805 # if defined JWXYZ_GL && !defined USE_IPHONE
806 CFRelease (xwindow->ogl_ctx);
808 CFRelease (xwindow->window.view);
812 // setup_p = NO; // #### wait, do we need this?
819 [crash_timer invalidate];
821 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
822 [prefs removeObjectForKey:@"wasRunning"];
824 # endif // USE_IPHONE
826 [super stopAnimation];
828 // When an animation is no longer running (e.g., looking at the list)
829 // then it's ok to power off the screen when docked.
832 [UIApplication sharedApplication].idleTimerDisabled = NO;
835 // Without this, the GL frame stays on screen when switching tabs
836 // in System Preferences.
837 // (Or perhaps it used to. It doesn't seem to matter on 10.9.)
840 [NSOpenGLContext clearCurrentContext];
841 # endif // !USE_IPHONE
843 clear_gl_error(); // This hack is defunct, don't let this linger.
846 CGContextRelease (backbuffer);
850 munmap (backbuffer_data, backbuffer_len);
851 backbuffer_data = NULL;
857 [touchbar_view stopAnimation];
858 [touchbar_window close];
860 [touchbar_view release];
861 [touchbar_window release];
864 touchbar_window = nil;
870 - (NSOpenGLContext *) oglContext
876 // #### maybe this could/should just be on 'lockFocus' instead?
877 - (void) prepareContext
881 [EAGLContext setCurrentContext:ogl_ctx];
883 [ogl_ctx makeCurrentContext];
884 // check_gl_error ("makeCurrentContext");
885 #endif // !USE_IPHONE
888 xwindow->window.current_drawable = xwindow;
895 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
897 fps_compute (fpst, 0, -1);
902 /* Some of the older X11 savers look bad if a "pixel" is not a thing you can
903 see. They expect big, chunky, luxurious 1990s pixels, and if they use
904 "device" pixels on a Retina screen, everything just disappears.
906 Retina iPads have 768x1024 point screens which are 1536x2048 pixels,
907 2017 iMac screens are 5120x2880 in device pixels.
909 This method is overridden in XScreenSaverGLView, since this kludge
910 isn't necessary for GL programs, being resolution independent by
913 - (CGFloat) hackedContentScaleFactor
916 CGFloat s = self.contentScaleFactor;
918 CGFloat s = self.window.backingScaleFactor;
922 NSSize b = [self bounds].size;
923 CGFloat wh = b.width > b.height ? b.width : b.height;
924 const int max = 800; // maybe 1024?
937 current_device_rotation (void)
939 UIDeviceOrientation o = [[UIDevice currentDevice] orientation];
941 /* Sometimes UIDevice doesn't know the proper orientation, or the device is
942 face up/face down, so in those cases fall back to the status bar
943 orientation. The SaverViewController tries to set the status bar to the
944 proper orientation before it creates the XScreenSaverView; see
945 _storedOrientation in SaverViewController.
947 if (o == UIDeviceOrientationUnknown ||
948 o == UIDeviceOrientationFaceUp ||
949 o == UIDeviceOrientationFaceDown) {
950 /* Mind the differences between UIInterfaceOrientation and
952 1. UIInterfaceOrientation does not include FaceUp and FaceDown.
953 2. LandscapeLeft and LandscapeRight are swapped between the two. But
954 converting between device and interface orientation doesn't need to
955 take this into account, because (from the UIInterfaceOrientation
956 description): "rotating the device requires rotating the content in
957 the opposite direction."
959 /* statusBarOrientation deprecated in iOS 9 */
960 o = (UIDeviceOrientation) // from UIInterfaceOrientation
961 [UIApplication sharedApplication].statusBarOrientation;
965 case UIDeviceOrientationLandscapeLeft: return -90; break;
966 case UIDeviceOrientationLandscapeRight: return 90; break;
967 case UIDeviceOrientationPortraitUpsideDown: return 180; break;
968 default: return 0; break;
973 - (void) handleException: (NSException *)e
975 NSLog (@"Caught exception: %@", e);
976 UIAlertController *c = [UIAlertController
977 alertControllerWithTitle:
978 [NSString stringWithFormat: @"%s crashed!",
980 message: [NSString stringWithFormat:
981 @"The error message was:"
983 "If it keeps crashing, try "
984 "resetting its options.",
986 preferredStyle:UIAlertControllerStyleAlert];
988 [c addAction: [UIAlertAction actionWithTitle: @"Exit"
989 style: UIAlertActionStyleDefault
990 handler: ^(UIAlertAction *a) {
993 [c addAction: [UIAlertAction actionWithTitle: @"Keep going"
994 style: UIAlertActionStyleDefault
995 handler: ^(UIAlertAction *a) {
996 [self stopAndClose:NO];
999 UIViewController *vc =
1000 [UIApplication sharedApplication].keyWindow.rootViewController;
1001 while (vc.presentedViewController)
1002 vc = vc.presentedViewController;
1003 [vc presentViewController:c animated:YES completion:nil];
1004 [self stopAnimation];
1007 #endif // USE_IPHONE
1016 // iOS always uses OpenGL ES 1.1.
1022 gl_check_ver (const struct gl_version *caps,
1026 return caps->major > gl_major ||
1027 (caps->major == gl_major && caps->minor >= gl_minor);
1032 /* Called during startAnimation before the first call to createBackbuffer. */
1033 - (void) enableBackbuffer:(CGSize)new_backbuffer_size
1036 struct gl_version version;
1039 const char *version_str = (const char *)glGetString (GL_VERSION);
1041 /* iPhone is always OpenGL ES 1.1. */
1042 if (sscanf ((const char *)version_str, "%u.%u",
1043 &version.major, &version.minor) < 2)
1051 // The OpenGL extensions in use in here are pretty are pretty much ubiquitous
1052 // on OS X, but it's still good form to check.
1053 const GLubyte *extensions = glGetString (GL_EXTENSIONS);
1055 glGenTextures (1, &backbuffer_texture);
1057 // On really old systems, it would make sense to split the texture
1060 gl_texture_target = (gluCheckExtension ((const GLubyte *)
1061 "GL_ARB_texture_rectangle",
1063 ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
1065 // OES_texture_npot also provides this, but iOS never provides it.
1066 gl_limited_npot_p = jwzgles_gluCheckExtension
1067 ((const GLubyte *) "GL_APPLE_texture_2D_limited_npot", extensions);
1068 gl_texture_target = GL_TEXTURE_2D;
1071 glBindTexture (gl_texture_target, backbuffer_texture);
1072 glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1073 // GL_LINEAR might make sense on Retina iPads.
1074 glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1075 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1076 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1079 // There isn't much sense in supporting one of these if the other
1081 gl_apple_client_storage_p =
1082 gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage",
1084 gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions);
1086 if (gl_apple_client_storage_p) {
1087 glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE,
1088 GL_STORAGE_SHARED_APPLE);
1089 glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
1093 // If a video adapter suports BGRA textures, then that's probably as fast as
1094 // you're gonna get for getting a texture onto the screen.
1097 jwzgles_gluCheckExtension
1098 ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888", extensions) ?
1102 gl_pixel_type = GL_UNSIGNED_BYTE;
1103 // See also OES_read_format.
1105 if (gl_check_ver (&version, 1, 2) ||
1106 (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) &&
1107 gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels",
1109 gl_pixel_format = GL_BGRA;
1110 // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV.
1111 gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1113 gl_pixel_format = GL_RGBA;
1114 gl_pixel_type = GL_UNSIGNED_BYTE;
1116 // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more
1117 // sense on PowerPC.
1120 glEnable (gl_texture_target);
1121 glEnableClientState (GL_VERTEX_ARRAY);
1122 glEnableClientState (GL_TEXTURE_COORD_ARRAY);
1124 check_gl_error ("enableBackbuffer");
1129 - (BOOL) suppressRotationAnimation
1131 return [self ignoreRotation]; // Don't animate if we aren't rotating
1134 - (BOOL) rotateTouches
1136 return FALSE; // Adjust event coordinates only if rotating
1141 - (void) setViewport
1143 # ifdef BACKBUFFER_OPENGL
1144 NSAssert ([NSOpenGLContext currentContext] ==
1145 ogl_ctx, @"invalid GL context");
1147 NSSize new_size = self.bounds.size;
1150 GLfloat s = self.contentScaleFactor;
1151 # else // !USE_IPHONE
1152 const GLfloat s = self.window.backingScaleFactor;
1154 GLfloat hs = self.hackedContentScaleFactor;
1156 // On OS X this almost isn't necessary, except for the ugly aliasing
1158 glViewport (0, 0, new_size.width * s, new_size.height * s);
1160 glMatrixMode (GL_PROJECTION);
1167 (-new_size.width * hs, new_size.width * hs,
1168 -new_size.height * hs, new_size.height * hs,
1172 if ([self ignoreRotation]) {
1173 int o = (int) -current_device_rotation();
1174 glRotatef (o, 0, 0, 1);
1176 # endif // USE_IPHONE
1177 # endif // BACKBUFFER_OPENGL
1181 /* Create a bitmap context into which we render everything.
1182 If the desired size has changed, re-created it.
1183 new_size is in rotated pixels, not points: the same size
1184 and shape as the X11 window as seen by the hacks.
1186 - (void) createBackbuffer:(CGSize)new_size
1188 CGSize osize = CGSizeZero;
1190 osize.width = CGBitmapContextGetWidth(backbuffer);
1191 osize.height = CGBitmapContextGetHeight(backbuffer);
1195 (int)osize.width == (int)new_size.width &&
1196 (int)osize.height == (int)new_size.height)
1199 CGContextRef ob = backbuffer;
1200 void *odata = backbuffer_data;
1201 GLsizei olen = backbuffer_len;
1203 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1204 NSLog(@"backbuffer %.0fx%.0f",
1205 new_size.width, new_size.height);
1208 /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
1209 <https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html>.
1211 iOS uses bog-standard glTexImage2D (for now).
1213 glMapBuffer is the standard way to get data from system RAM to video
1214 memory asynchronously and without a memcpy, but support for
1215 APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer),
1216 and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3
1217 (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with
1218 CGBitmapContext: glMapBuffer can return a different pointer on each
1219 call, but a CGBitmapContext doesn't allow its data pointer to be
1220 changed -- and recreating the context for a new pointer can be
1221 expensive (glyph caches get dumped, for instance).
1223 glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT,
1224 and these seem to allow mapping the buffer and leaving it where it is
1225 in client address space while OpenGL works with the buffer, but it
1226 requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for
1227 GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage.
1229 AMD_pinned_buffer provides the same advantage as glMapBufferRange, but
1230 Apple never implemented that one for OS X.
1233 backbuffer_data = NULL;
1234 gl_texture_w = (int)new_size.width;
1235 gl_texture_h = (int)new_size.height;
1237 NSAssert (gl_texture_target == GL_TEXTURE_2D
1239 || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT
1241 , @"unexpected GL texture target");
1244 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1246 if (!gl_limited_npot_p)
1249 gl_texture_w = (GLsizei) to_pow2 (gl_texture_w);
1250 gl_texture_h = (GLsizei) to_pow2 (gl_texture_h);
1253 GLsizei bytes_per_row = gl_texture_w * 4;
1255 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1256 // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
1257 // it will fall back to a memcpy.
1258 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24
1259 bytes_per_row = (bytes_per_row + 31) & ~31;
1260 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1262 backbuffer_len = bytes_per_row * gl_texture_h;
1263 if (backbuffer_len) // mmap requires this to be non-zero.
1264 backbuffer_data = mmap (NULL, backbuffer_len,
1265 PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED,
1268 BOOL alpha_first_p, order_little_p;
1270 if (gl_pixel_format == GL_BGRA) {
1271 alpha_first_p = YES;
1272 order_little_p = YES;
1274 } else if (gl_pixel_format == GL_ABGR_EXT) {
1276 order_little_p = YES; */
1278 NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format");
1280 order_little_p = NO;
1284 NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type");
1286 NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 ||
1287 gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV ||
1288 gl_pixel_type == GL_UNSIGNED_BYTE,
1289 @"unknown GL pixel type");
1291 #if defined __LITTLE_ENDIAN__
1292 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8;
1293 #elif defined __BIG_ENDIAN__
1294 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1296 # error Unknown byte order.
1299 if (gl_pixel_type == backwards_pixel_type)
1300 order_little_p ^= YES;
1303 CGBitmapInfo bitmap_info =
1304 (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) |
1305 (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
1307 backbuffer = CGBitmapContextCreate (backbuffer_data,
1308 (int)new_size.width,
1309 (int)new_size.height,
1314 NSAssert (backbuffer, @"unable to allocate back buffer");
1318 r.origin.x = r.origin.y = 0;
1320 CGContextSetGrayFillColor (backbuffer, 0, 1);
1321 CGContextFillRect (backbuffer, r);
1323 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1324 if (gl_apple_client_storage_p)
1325 glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data);
1326 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1329 // Restore old bits, as much as possible, to the X11 upper left origin.
1331 CGRect rect; // pixels, not points
1333 rect.origin.y = (new_size.height - osize.height);
1336 CGImageRef img = CGBitmapContextCreateImage (ob);
1337 CGContextDrawImage (backbuffer, rect, img);
1338 CGImageRelease (img);
1339 CGContextRelease (ob);
1342 // munmap should round len up to the nearest page.
1343 munmap (odata, olen);
1346 check_gl_error ("createBackbuffer");
1350 - (void) drawBackbuffer
1352 # ifdef BACKBUFFER_OPENGL
1354 NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]],
1355 @"ogl_ctx is not an NSOpenGLContext");
1357 NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4),
1358 @"improperly-aligned backbuffer");
1360 // This gets width and height from the backbuffer in case
1361 // APPLE_client_storage is in use. See the note in createBackbuffer.
1362 // This still has to happen every frame even when APPLE_client_storage has
1363 // the video adapter pulling texture data straight from
1364 // XScreenSaverView-owned memory.
1365 glTexImage2D (gl_texture_target, 0, GL_RGBA,
1366 (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4),
1367 gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
1370 GLfloat w = xwindow->frame.width, h = xwindow->frame.height;
1372 GLfloat vertices[4][2] = {{-w, h}, {w, h}, {w, -h}, {-w, -h}};
1374 GLfloat tex_coords[4][2];
1377 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1378 # endif // USE_IPHONE
1384 tex_coords[0][0] = 0;
1385 tex_coords[0][1] = 0;
1386 tex_coords[1][0] = w;
1387 tex_coords[1][1] = 0;
1388 tex_coords[2][0] = w;
1389 tex_coords[2][1] = h;
1390 tex_coords[3][0] = 0;
1391 tex_coords[3][1] = h;
1393 glVertexPointer (2, GL_FLOAT, 0, vertices);
1394 glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
1395 glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
1397 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1398 check_gl_error ("drawBackbuffer");
1400 # endif // BACKBUFFER_OPENGL
1403 #endif // JWXYZ_QUARTZ
1407 - (void)enableBackbuffer:(CGSize)new_backbuffer_size;
1409 jwxyz_set_matrices (new_backbuffer_size.width, new_backbuffer_size.height);
1410 check_gl_error ("enableBackbuffer");
1413 - (void)createBackbuffer:(CGSize)new_size
1415 NSAssert ([NSOpenGLContext currentContext] ==
1416 ogl_ctx, @"invalid GL context");
1417 NSAssert (xwindow->window.current_drawable == xwindow,
1418 @"current_drawable not set properly");
1421 /* On iOS, Retina means glViewport gets called with the screen size instead
1422 of the backbuffer/xwindow size. This happens in startAnimation.
1424 The GL screenhacks call glViewport themselves.
1426 glViewport (0, 0, new_size.width, new_size.height);
1429 // TODO: Preserve contents on resize.
1430 glClear (GL_COLOR_BUFFER_BIT);
1431 check_gl_error ("createBackbuffer");
1437 - (void)flushBackbuffer
1440 // Make sure the right context is active: there's two under JWXYZ_GL.
1441 jwxyz_bind_drawable (xwindow, xwindow);
1446 # ifdef JWXYZ_QUARTZ
1447 // The OpenGL pipeline is not automatically synchronized with the contents
1448 // of the backbuffer, so without glFinish, OpenGL can start rendering from
1449 // the backbuffer texture at the same time that JWXYZ is clearing and
1450 // drawing the next frame in the backing store for the backbuffer texture.
1451 // This is only a concern under JWXYZ_QUARTZ because of
1452 // APPLE_client_storage; JWXYZ_GL doesn't use that.
1454 # endif // JWXYZ_QUARTZ
1456 // If JWXYZ_GL was single-buffered, there would need to be a glFinish (or
1457 // maybe just glFlush?) here, because single-buffered contexts don't always
1458 // update what's on the screen after drawing finishes. (i.e., in safe mode)
1460 # ifdef JWXYZ_QUARTZ
1461 // JWXYZ_GL is always double-buffered.
1462 if (double_buffered_p)
1463 # endif // JWXYZ_QUARTZ
1464 [ogl_ctx flushBuffer]; // despite name, this actually swaps
1465 # else // USE_IPHONE
1467 // jwxyz_bind_drawable() only binds the framebuffer, not the renderbuffer.
1469 GLint gl_renderbuffer = xwindow->gl_renderbuffer;
1472 glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
1473 [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
1474 # endif // USE_IPHONE
1476 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1477 // glGetError waits for the OpenGL command pipe to flush, so skip it in
1479 // OpenGL Programming Guide for Mac -> OpenGL Application Design
1480 // Strategies -> Allow OpenGL to Manage Your Resources
1481 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7
1482 check_gl_error ("flushBackbuffer");
1487 /* Inform X11 that the size of our window has changed.
1491 if (!xdpy) return; // early
1493 NSSize new_size; // pixels, not points
1495 new_size = self.bounds.size;
1499 // If this hack ignores rotation, then that means that it pretends to
1500 // always be in portrait mode. If the View has been resized to a
1501 // landscape shape, swap width and height to keep the backbuffer
1504 double rot = current_device_rotation();
1505 if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
1506 CGFloat swap = new_size.width;
1507 new_size.width = new_size.height;
1508 new_size.height = swap;
1510 # endif // USE_IPHONE
1512 double s = self.hackedContentScaleFactor;
1513 new_size.width *= s;
1514 new_size.height *= s;
1516 [self prepareContext];
1519 // On first resize, xwindow->frame is 0x0.
1520 if (xwindow->frame.width == new_size.width &&
1521 xwindow->frame.height == new_size.height)
1524 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1526 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1528 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
1529 xwindow->frame.x = 0;
1530 xwindow->frame.y = 0;
1531 xwindow->frame.width = new_size.width;
1532 xwindow->frame.height = new_size.height;
1534 [self createBackbuffer:CGSizeMake(xwindow->frame.width,
1535 xwindow->frame.height)];
1537 # if defined JWXYZ_QUARTZ
1538 xwindow->cgc = backbuffer;
1539 NSAssert (xwindow->cgc, @"no CGContext");
1540 # elif defined JWXYZ_GL && !defined USE_IPHONE
1542 [ogl_ctx setView:xwindow->window.view]; // (Is this necessary?)
1543 # endif // JWXYZ_GL && USE_IPHONE
1545 jwxyz_window_resized (xdpy);
1547 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1548 NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
1551 // Next time render_x11 is called, run the saver's reshape_cb.
1558 /* Called by SaverRunner when the device has changed orientation.
1559 That means we need to generate a resize event, even if the size
1560 has not changed (e.g., from LandscapeLeft to LandscapeRight).
1562 - (void) orientationChanged
1566 next_frame_time = 0; // Get a new frame on screen quickly
1569 /* A hook run after the 'reshape_' method has been called. Used by
1570 XScreenSaverGLView to adjust the in-scene GL viewport.
1572 - (void) postReshape
1575 #endif // USE_IPHONE
1578 // Only render_x11 should call this. XScreenSaverGLView specializes it.
1579 - (void) reshape_x11
1581 xsft->reshape_cb (xdpy, xwindow, xdata,
1582 xwindow->frame.width, xwindow->frame.height);
1591 // jwxyz_make_display needs this.
1592 [self prepareContext]; // resize_x11 also calls this.
1599 # ifdef JWXYZ_QUARTZ
1600 xwindow->cgc = backbuffer;
1601 # endif // JWXYZ_QUARTZ
1602 xdpy = jwxyz_quartz_make_display (xwindow);
1604 # if defined USE_IPHONE
1605 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
1608 TRUE; // Rotation doesn't work yet. TODO: Make rotation work.
1610 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
1611 # endif // !JWXYZ_GL
1612 # endif // USE_IPHONE
1614 _lowrez_p = get_boolean_resource (xdpy, "lowrez", "Lowrez");
1618 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1619 NSSize b = [self bounds].size;
1620 CGFloat s = self.hackedContentScaleFactor;
1622 CGFloat o = self.contentScaleFactor;
1624 CGFloat o = self.window.backingScaleFactor;
1626 NSLog(@"lowrez: scaling %.0fx%.0f -> %.0fx%.0f (%.02f)",
1627 b.width * o, b.height * o,
1628 b.width * s, b.height * s, s);
1638 xsft->setup_cb (xsft, xsft->setup_arg);
1641 NSAssert(!xdata, @"xdata already initialized");
1644 # undef ya_rand_init
1647 XSetWindowBackground (xdpy, xwindow,
1648 get_pixel_resource (xdpy, 0,
1649 "background", "Background"));
1650 XClearWindow (xdpy, xwindow);
1653 [[self window] setAcceptsMouseMovedEvents:YES];
1656 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
1657 drawing primitives will run on the GPU instead of the CPU.
1658 It seems like it might make things worse rather than better,
1659 though... Plus it makes us binary-incompatible with 10.4.
1661 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
1662 [[self window] setPreferredBackingLocation:
1663 NSWindowBackingLocationVideoMemory];
1667 /* Kludge: even though the init_cb functions are declared to take 2 args,
1668 actually call them with 3, for the benefit of xlockmore_init() and
1671 void *(*init_cb) (Display *, Window, void *) =
1672 (void *(*) (Display *, Window, void *)) xsft->init_cb;
1674 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
1675 // NSAssert(xdata, @"no xdata from init");
1676 if (! xdata) abort();
1678 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1679 fpst = fps_init (xdpy, xwindow);
1680 fps_cb = xsft->fps_cb;
1681 if (! fps_cb) fps_cb = screenhack_do_fps;
1688 if (current_device_rotation() != 0) // launched while rotated
1692 [self checkForUpdates];
1696 /* I don't understand why we have to do this *every frame*, but we do,
1697 or else the cursor comes back on.
1700 if (![self isPreview])
1701 [NSCursor setHiddenUntilMouseMoves:YES];
1707 /* This is just a guess, but the -fps code wants to know how long
1708 we were sleeping between frames.
1710 long usecs = 1000000 * [self animationTimeInterval];
1711 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
1712 if (usecs < 0) usecs = 0;
1713 fps_slept (fpst, usecs);
1717 /* Run any XtAppAddInput and XtAppAddTimeOut callbacks now.
1718 Do this before delaying for next_frame_time to avoid throttling
1719 timers to the hack's frame rate.
1721 XtAppProcessEvent (XtDisplayToApplicationContext (xdpy),
1722 XtIMTimer | XtIMAlternateInput);
1725 /* It turns out that on some systems (possibly only 10.5 and older?)
1726 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
1727 that we cannot rely on it.
1729 Some of the screen hacks want to delay for long periods, and letting the
1730 framework run the update function at 30 FPS when it really wanted half a
1731 minute between frames would be bad. So instead, we assume that the
1732 framework's animation timer might fire whenever, but we only invoke the
1733 screen hack's "draw frame" method when enough time has expired.
1735 This means two extra calls to gettimeofday() per frame. For fast-cycling
1736 screen savers, that might actually slow them down. Oh well.
1738 A side-effect of this is that it's not possible for a saver to request
1739 an animation interval that is faster than animationTimeInterval.
1741 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
1742 ignored, it's important that it be faster than 30 FPS. 240 FPS is good.
1744 An NSTimer won't fire if the timer is already running the invocation
1745 function from a previous firing. So, if we use a 30 FPS
1746 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
1747 frame, there will be a 26666 µs delay until the next frame, 66666 µs
1748 after the beginning of the current frame. In other words, 25 FPS
1751 Frame rates tend to snap to values of 30/N, where N is a positive
1752 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1753 is rounded down from what it would normally be.
1755 So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates
1756 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1757 steps for higher or lower animation time intervals respectively.
1760 gettimeofday (&tv, 0);
1761 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1762 if (now < next_frame_time) return;
1764 // [self flushBackbuffer];
1767 // We do this here instead of in setFrame so that all the
1768 // Xlib drawing takes place under the animation timer.
1772 [ogl_ctx setView:self];
1773 # endif // !USE_IPHONE
1782 // NSAssert(xdata, @"no xdata when drawing");
1783 if (! xdata) abort();
1784 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1786 fps_cb (xdpy, xwindow, fpst, xdata);
1788 gettimeofday (&tv, 0);
1789 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1790 next_frame_time = now + (delay / 1000000.0);
1792 # ifdef JWXYZ_QUARTZ
1793 [self drawBackbuffer];
1795 // This can also happen near the beginning of render_x11.
1796 [self flushBackbuffer];
1798 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1799 if (delay < [self animationTimeInterval])
1800 [self setAnimationTimeInterval:(delay / 1000000.0)];
1803 # ifdef DO_GC_HACKERY
1804 /* Current theory is that the 10.6 garbage collector sucks in the
1807 It only does a collection when a threshold of outstanding
1808 collectable allocations has been surpassed. However, CoreGraphics
1809 creates lots of small collectable allocations that contain pointers
1810 to very large non-collectable allocations: a small CG object that's
1811 collectable referencing large malloc'd allocations (non-collectable)
1812 containing bitmap data. So the large allocation doesn't get freed
1813 until GC collects the small allocation, which triggers its finalizer
1814 to run which frees the large allocation. So GC is deciding that it
1815 doesn't really need to run, even though the process has gotten
1816 enormous. GC eventually runs once pageouts have happened, but by
1817 then it's too late, and the machine's resident set has been
1820 So, we force an exhaustive garbage collection in this process
1821 approximately every 5 seconds whether the system thinks it needs
1825 static int tick = 0;
1826 if (++tick > 5*30) {
1828 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1831 # endif // DO_GC_HACKERY
1835 @catch (NSException *e) {
1836 [self handleException: e];
1838 # endif // USE_IPHONE
1842 - (void) animateOneFrame
1844 // Render X11 into the backing store bitmap...
1846 # ifdef JWXYZ_QUARTZ
1847 NSAssert (backbuffer, @"no back buffer");
1850 UIGraphicsPushContext (backbuffer);
1852 # endif // JWXYZ_QUARTZ
1856 # if defined USE_IPHONE && defined JWXYZ_QUARTZ
1857 UIGraphicsPopContext();
1860 # ifdef USE_TOUCHBAR
1861 if (touchbar_view) [touchbar_view animateOneFrame];
1866 # ifndef USE_IPHONE // Doesn't exist on iOS
1868 - (void) setFrame:(NSRect) newRect
1870 [super setFrame:newRect];
1872 if (xwindow) // inform Xlib that the window has changed now.
1876 - (void) setFrameSize:(NSSize) newSize
1878 [super setFrameSize:newSize];
1883 # else // USE_IPHONE
1885 - (void) layoutSubviews
1887 [super layoutSubviews];
1896 +(BOOL) performGammaFade
1901 - (BOOL) hasConfigureSheet
1906 + (NSString *) decompressXML: (NSData *)data
1908 if (! data) return 0;
1909 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1911 // If it's not already XML, decompress it.
1912 NSAssert (compressed_p, @"xml isn't compressed");
1914 NSMutableData *data2 = 0;
1917 memset (&zs, 0, sizeof(zs));
1918 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1920 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1921 data2 = [NSMutableData dataWithLength: usize];
1922 zs.next_in = (Bytef *) data.bytes;
1923 zs.avail_in = (uint) data.length;
1924 zs.next_out = (Bytef *) data2.bytes;
1925 zs.avail_out = (uint) data2.length;
1926 ret = inflate (&zs, Z_FINISH);
1929 if (ret == Z_OK || ret == Z_STREAM_END)
1932 NSAssert2 (0, @"gunzip error: %d: %s",
1933 ret, (zs.msg ? zs.msg : "<null>"));
1936 NSString *s = [[NSString alloc]
1937 initWithData:data encoding:NSUTF8StringEncoding];
1944 - (NSWindow *) configureSheet
1946 - (UIViewController *) configureView
1949 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1950 NSString *file = [NSString stringWithCString:xsft->progclass
1951 encoding:NSISOLatin1StringEncoding];
1952 file = [file lowercaseString];
1953 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1955 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1956 file, [bundle resourcePath]);
1961 UIViewController *sheet;
1962 # else // !USE_IPHONE
1964 # endif // !USE_IPHONE
1966 NSData *xmld = [NSData dataWithContentsOfFile:path];
1967 NSString *xml = [[self class] decompressXML: xmld];
1968 sheet = [[XScreenSaverConfigSheet alloc]
1969 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1970 options:xsft->options
1971 controller:[prefsReader userDefaultsController]
1972 globalController:[prefsReader globalDefaultsController]
1973 defaults:[prefsReader defaultOptions]];
1975 // #### am I expected to retain this, or not? wtf.
1976 // I thought not, but if I don't do this, we (sometimes) crash.
1977 // #### Analyze says "potential leak of an object stored into sheet"
1984 - (NSUserDefaultsController *) userDefaultsController
1986 return [prefsReader userDefaultsController];
1990 /* Announce our willingness to accept keyboard input.
1992 - (BOOL)acceptsFirstResponder
2002 # else // USE_IPHONE
2004 // There's no way to play a standard system alert sound!
2005 // We'd have to include our own WAV for that.
2007 // Or we could vibrate:
2008 // #import <AudioToolbox/AudioToolbox.h>
2009 // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
2011 // Instead, just flash the screen white, then fade.
2013 UIView *v = [[UIView alloc] initWithFrame: [self frame]];
2014 [v setBackgroundColor: [UIColor whiteColor]];
2015 [[self window] addSubview:v];
2016 [UIView animateWithDuration: 0.1
2017 animations:^{ [v setAlpha: 0.0]; }
2018 completion:^(BOOL finished) { [v removeFromSuperview]; } ];
2020 # endif // USE_IPHONE
2024 /* Send an XEvent to the hack. Returns YES if it was handled.
2026 - (BOOL) sendEvent: (XEvent *) e
2028 if (!initted_p || ![self isAnimating]) // no event handling unless running.
2032 [self prepareContext];
2033 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
2041 /* Convert an NSEvent into an XEvent, and pass it along.
2042 Returns YES if it was handled.
2044 - (BOOL) convertEvent: (NSEvent *) e
2048 memset (&xe, 0, sizeof(xe));
2052 int flags = [e modifierFlags];
2053 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
2054 if (flags & NSShiftKeyMask) state |= ShiftMask;
2055 if (flags & NSControlKeyMask) state |= ControlMask;
2056 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
2057 if (flags & NSCommandKeyMask) state |= Mod2Mask;
2059 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
2061 double s = [self hackedContentScaleFactor];
2063 int y = s * ([self bounds].size.height - p.y);
2065 xe.xany.type = type;
2071 xe.xbutton.state = state;
2072 if ([e type] == NSScrollWheel)
2073 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
2074 [e deltaY] < 0 ? Button5 :
2075 [e deltaX] > 0 ? Button6 :
2076 [e deltaX] < 0 ? Button7 :
2079 xe.xbutton.button = (unsigned int) [e buttonNumber] + 1;
2084 xe.xmotion.state = state;
2089 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
2090 [e charactersIgnoringModifiers]);
2093 if (!ns || [ns length] == 0) // dead key
2095 // Cocoa hides the difference between left and right keys.
2096 // Also we only get KeyPress events for these, no KeyRelease
2097 // (unless we hack the mod state manually. Bleh.)
2099 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
2100 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
2101 else if (flags & NSControlKeyMask) k = XK_Control_L;
2102 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
2103 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
2105 else if ([ns length] == 1) // real key
2107 switch ([ns characterAtIndex:0]) {
2108 case NSLeftArrowFunctionKey: k = XK_Left; break;
2109 case NSRightArrowFunctionKey: k = XK_Right; break;
2110 case NSUpArrowFunctionKey: k = XK_Up; break;
2111 case NSDownArrowFunctionKey: k = XK_Down; break;
2112 case NSPageUpFunctionKey: k = XK_Page_Up; break;
2113 case NSPageDownFunctionKey: k = XK_Page_Down; break;
2114 case NSHomeFunctionKey: k = XK_Home; break;
2115 case NSPrevFunctionKey: k = XK_Prior; break;
2116 case NSNextFunctionKey: k = XK_Next; break;
2117 case NSBeginFunctionKey: k = XK_Begin; break;
2118 case NSEndFunctionKey: k = XK_End; break;
2119 case NSF1FunctionKey: k = XK_F1; break;
2120 case NSF2FunctionKey: k = XK_F2; break;
2121 case NSF3FunctionKey: k = XK_F3; break;
2122 case NSF4FunctionKey: k = XK_F4; break;
2123 case NSF5FunctionKey: k = XK_F5; break;
2124 case NSF6FunctionKey: k = XK_F6; break;
2125 case NSF7FunctionKey: k = XK_F7; break;
2126 case NSF8FunctionKey: k = XK_F8; break;
2127 case NSF9FunctionKey: k = XK_F9; break;
2128 case NSF10FunctionKey: k = XK_F10; break;
2129 case NSF11FunctionKey: k = XK_F11; break;
2130 case NSF12FunctionKey: k = XK_F12; break;
2134 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
2135 k = (ss && *ss ? *ss : 0);
2141 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
2143 xe.xkey.keycode = k;
2144 xe.xkey.state = state;
2148 NSAssert1 (0, @"unknown X11 event type: %d", type);
2152 return [self sendEvent: &xe];
2156 - (void) mouseDown: (NSEvent *) e
2158 if (! [self convertEvent:e type:ButtonPress])
2159 [super mouseDown:e];
2162 - (void) mouseUp: (NSEvent *) e
2164 if (! [self convertEvent:e type:ButtonRelease])
2168 - (void) otherMouseDown: (NSEvent *) e
2170 if (! [self convertEvent:e type:ButtonPress])
2171 [super otherMouseDown:e];
2174 - (void) otherMouseUp: (NSEvent *) e
2176 if (! [self convertEvent:e type:ButtonRelease])
2177 [super otherMouseUp:e];
2180 - (void) mouseMoved: (NSEvent *) e
2182 if (! [self convertEvent:e type:MotionNotify])
2183 [super mouseMoved:e];
2186 - (void) mouseDragged: (NSEvent *) e
2188 if (! [self convertEvent:e type:MotionNotify])
2189 [super mouseDragged:e];
2192 - (void) otherMouseDragged: (NSEvent *) e
2194 if (! [self convertEvent:e type:MotionNotify])
2195 [super otherMouseDragged:e];
2198 - (void) scrollWheel: (NSEvent *) e
2200 if (! [self convertEvent:e type:ButtonPress])
2201 [super scrollWheel:e];
2204 - (void) keyDown: (NSEvent *) e
2206 if (! [self convertEvent:e type:KeyPress])
2210 - (void) keyUp: (NSEvent *) e
2212 if (! [self convertEvent:e type:KeyRelease])
2216 - (void) flagsChanged: (NSEvent *) e
2218 if (! [self convertEvent:e type:KeyPress])
2219 [super flagsChanged:e];
2223 - (NSOpenGLPixelFormat *) getGLPixelFormat
2225 NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
2227 NSOpenGLPixelFormatAttribute attrs[40];
2229 attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
2231 /* OpenGL's core profile removes a lot of the same stuff that was removed in
2232 OpenGL ES (e.g. glBegin, glDrawPixels), so it might be a possibility.
2234 opengl_core_p = True;
2235 if (opengl_core_p) {
2236 attrs[i++] = NSOpenGLPFAOpenGLProfile;
2237 attrs[i++] = NSOpenGLProfileVersion3_2Core;
2241 /* Eventually: multisampled pixmaps. May not be supported everywhere.
2242 if (multi_sample_p) {
2243 attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1;
2244 attrs[i++] = NSOpenGLPFASamples; attrs[i++] = 6;
2248 # ifdef JWXYZ_QUARTZ
2249 // Under Quartz, we're just blitting a texture.
2250 if (double_buffered_p)
2251 attrs[i++] = NSOpenGLPFADoubleBuffer;
2255 /* Under OpenGL, all sorts of drawing commands are being issued, and it might
2256 be a performance problem if this activity occurs on the front buffer.
2257 Also, some screenhacks expect OS X/iOS to always double-buffer.
2258 NSOpenGLPFABackingStore prevents flickering with screenhacks that
2259 don't redraw the entire screen every frame.
2261 attrs[i++] = NSOpenGLPFADoubleBuffer;
2262 attrs[i++] = NSOpenGLPFABackingStore;
2265 attrs[i++] = NSOpenGLPFAWindow;
2267 attrs[i++] = NSOpenGLPFAPixelBuffer;
2268 /* ...But not NSOpenGLPFAFullScreen, because that would be for
2269 [NSOpenGLContext setFullScreen].
2273 /* NSOpenGLPFAFullScreen would go here if initWithFrame's isPreview == NO.
2278 NSOpenGLPixelFormat *p = [[NSOpenGLPixelFormat alloc]
2279 initWithAttributes:attrs];
2287 - (void) stopAndClose
2289 [self stopAndClose:NO];
2293 - (void) stopAndClose:(Bool)relaunch_p
2295 if ([self isAnimating])
2296 [self stopAnimation];
2298 /* Need to make the SaverListController be the firstResponder again
2299 so that it can continue to receive its own shake events. I
2300 suppose that this abstraction-breakage means that I'm adding
2301 XScreenSaverView to the UINavigationController wrong...
2303 // UIViewController *v = [[self window] rootViewController];
2304 // if ([v isKindOfClass: [UINavigationController class]]) {
2305 // UINavigationController *n = (UINavigationController *) v;
2306 // [[n topViewController] becomeFirstResponder];
2308 [self resignFirstResponder];
2310 if (relaunch_p) { // Fake a shake on the SaverListController.
2311 [_delegate didShake:self];
2312 } else { // Not launching another, animate our return to the list.
2313 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
2314 NSLog (@"fading back to saver list");
2316 [_delegate wantsFadeOut:self];
2321 /* We distinguish between taps and drags.
2323 - Drags/pans (down, motion, up) are sent to the saver to handle.
2324 - Single-taps are sent to the saver to handle.
2325 - Double-taps are sent to the saver as a "Space" keypress.
2326 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
2327 - All taps expose the momentary "Close" button.
2330 - (void)initGestures
2332 UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
2334 action:@selector(handleDoubleTap)];
2335 dtap.numberOfTapsRequired = 2;
2336 dtap.numberOfTouchesRequired = 1;
2338 UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
2340 action:@selector(handleTap:)];
2341 stap.numberOfTapsRequired = 1;
2342 stap.numberOfTouchesRequired = 1;
2344 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
2346 action:@selector(handlePan:)];
2347 pan.maximumNumberOfTouches = 1;
2348 pan.minimumNumberOfTouches = 1;
2350 // I couldn't get Swipe to work, but using a second Pan recognizer works.
2351 UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
2353 action:@selector(handlePan2:)];
2354 pan2.maximumNumberOfTouches = 2;
2355 pan2.minimumNumberOfTouches = 2;
2357 // Also handle long-touch, and treat that the same as Pan.
2358 // Without this, panning doesn't start until there's motion, so the trick
2359 // of holding down your finger to freeze the scene doesn't work.
2361 UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
2363 action:@selector(handleLongPress:)];
2364 hold.numberOfTapsRequired = 0;
2365 hold.numberOfTouchesRequired = 1;
2366 hold.minimumPressDuration = 0.25; /* 1/4th second */
2368 // Two finger pinch to zoom in on the view.
2369 UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]
2371 action:@selector(handlePinch:)];
2373 [stap requireGestureRecognizerToFail: dtap];
2374 [stap requireGestureRecognizerToFail: hold];
2375 [dtap requireGestureRecognizerToFail: hold];
2376 [pan requireGestureRecognizerToFail: hold];
2377 [pan2 requireGestureRecognizerToFail: pinch];
2379 [self setMultipleTouchEnabled:YES];
2381 [self addGestureRecognizer: dtap];
2382 [self addGestureRecognizer: stap];
2383 [self addGestureRecognizer: pan];
2384 [self addGestureRecognizer: pan2];
2385 [self addGestureRecognizer: hold];
2386 [self addGestureRecognizer: pinch];
2397 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
2398 convert it to what X11 and OpenGL expect.
2400 Getting this crap right is tricky, given the confusion of the various
2401 scale factors, so here's a checklist that I think covers all of the X11
2402 and OpenGL cases. For each of these: rotate to all 4 orientations;
2403 ensure the mouse tracks properly to all 4 corners.
2405 Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
2407 Test hacks must cover:
2408 X11 ignoreRotation = true
2409 X11 ignoreRotation = false
2410 OpenGL (rotation is handled manually, so they never ignoreRotation)
2412 Test devices must cover:
2413 contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
2414 contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
2415 contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
2417 iPad 2: 768x1024 / 1 = 768x1024
2418 iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
2419 iPhone 4s: 640x960 / 2 = 320x480
2420 iPhone 5: 640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
2421 iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
2424 iPad2 iPadAir iPhone4s iPhone5 iPhone6+
2425 Attraction X yes - - - - Y
2426 Fireworkx X no - - - - Y
2427 Carousel GL yes - - - - Y
2428 Voronoi GL no - - - - -
2430 - (void) convertMouse:(CGPoint *)p
2432 CGFloat xx = p->x, yy = p->y;
2434 # if 0 // TARGET_IPHONE_SIMULATOR
2436 XWindowAttributes xgwa;
2437 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2438 NSLog (@"TOUCH %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2440 xgwa.width, xgwa.height,
2441 [self contentScaleFactor],
2442 [self hackedContentScaleFactor],
2443 [self rotateTouches], [self ignoreRotation]);
2445 # endif // TARGET_IPHONE_SIMULATOR
2447 if ([self rotateTouches]) {
2449 // The XScreenSaverGLView case:
2450 // The X11 window is rotated, as is the framebuffer.
2451 // The device coordinates match the framebuffer dimensions,
2452 // but might have axes swapped... and we need to swap them
2455 int w = [self frame].size.width;
2456 int h = [self frame].size.height;
2457 GLfloat xr = (GLfloat) xx / w;
2458 GLfloat yr = (GLfloat) yy / h;
2460 int o = (int) current_device_rotation();
2462 case -90: case 270: swap = xr; xr = 1-yr; yr = swap; break;
2463 case 90: case -270: swap = xr; xr = yr; yr = 1-swap; break;
2464 case 180: case -180: xr = 1-xr; yr = 1-yr; break;
2470 } else if ([self ignoreRotation]) {
2472 // The X11 case, where the hack has opted not to rotate:
2473 // The X11 window is unrotated, but the framebuffer is rotated.
2474 // The device coordinates match the framebuffer, so they need to
2475 // be de-rotated to match the X11 window.
2477 int w = [self frame].size.width;
2478 int h = [self frame].size.height;
2480 int o = (int) current_device_rotation();
2482 case -90: case 270: swap = xx; xx = h-yy; yy = swap; break;
2483 case 90: case -270: swap = xx; xx = yy; yy = w-swap; break;
2484 case 180: case -180: xx = w-xx; yy = h-yy; break;
2489 double s = [self hackedContentScaleFactor];
2493 # if 0 // TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
2495 XWindowAttributes xgwa;
2496 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2497 NSLog (@"touch %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2499 xgwa.width, xgwa.height,
2500 [self contentScaleFactor],
2501 [self hackedContentScaleFactor],
2502 [self rotateTouches], [self ignoreRotation]);
2503 if (p->x < 0 || p->y < 0 || p->x > xgwa.width || p->y > xgwa.height)
2506 # endif // TARGET_IPHONE_SIMULATOR
2510 /* Single click exits saver.
2512 - (void) handleTap:(UIGestureRecognizer *)sender
2518 memset (&xe, 0, sizeof(xe));
2520 [self showCloseButton];
2522 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2523 [self convertMouse:&p];
2524 NSAssert (xwindow->type == WINDOW, @"not a window");
2525 xwindow->window.last_mouse_x = p.x;
2526 xwindow->window.last_mouse_y = p.y;
2528 xe.xany.type = ButtonPress;
2529 xe.xbutton.button = 1;
2533 if (! [self sendEvent: &xe])
2536 xe.xany.type = ButtonRelease;
2537 xe.xbutton.button = 1;
2541 [self sendEvent: &xe];
2545 /* Double click sends Space KeyPress.
2547 - (void) handleDoubleTap
2549 if (!xsft->event_cb || !xwindow) return;
2551 [self showCloseButton];
2554 memset (&xe, 0, sizeof(xe));
2555 xe.xkey.keycode = ' ';
2556 xe.xany.type = KeyPress;
2557 BOOL ok1 = [self sendEvent: &xe];
2558 xe.xany.type = KeyRelease;
2559 BOOL ok2 = [self sendEvent: &xe];
2565 /* Drag with one finger down: send MotionNotify.
2567 - (void) handlePan:(UIGestureRecognizer *)sender
2569 if (!xsft->event_cb || !xwindow) return;
2571 [self showCloseButton];
2574 memset (&xe, 0, sizeof(xe));
2576 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2577 [self convertMouse:&p];
2578 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
2579 xwindow->window.last_mouse_x = p.x;
2580 xwindow->window.last_mouse_y = p.y;
2582 switch (sender.state) {
2583 case UIGestureRecognizerStateBegan:
2584 xe.xany.type = ButtonPress;
2585 xe.xbutton.button = 1;
2590 case UIGestureRecognizerStateEnded:
2591 xe.xany.type = ButtonRelease;
2592 xe.xbutton.button = 1;
2597 case UIGestureRecognizerStateChanged:
2598 xe.xany.type = MotionNotify;
2607 BOOL ok = [self sendEvent: &xe];
2608 if (!ok && xe.xany.type == ButtonRelease)
2613 /* Hold one finger down: assume we're about to start dragging.
2614 Treat the same as Pan.
2616 - (void) handleLongPress:(UIGestureRecognizer *)sender
2618 [self handlePan:sender];
2623 /* Drag with 2 fingers down: send arrow keys.
2625 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2627 if (!xsft->event_cb || !xwindow) return;
2629 [self showCloseButton];
2631 if (sender.state != UIGestureRecognizerStateEnded)
2635 memset (&xe, 0, sizeof(xe));
2637 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2638 [self convertMouse:&p];
2640 if (fabs(p.x) > fabs(p.y))
2641 xe.xkey.keycode = (p.x > 0 ? XK_Right : XK_Left);
2643 xe.xkey.keycode = (p.y > 0 ? XK_Down : XK_Up);
2645 BOOL ok1 = [self sendEvent: &xe];
2646 xe.xany.type = KeyRelease;
2647 BOOL ok2 = [self sendEvent: &xe];
2653 /* Pinch with 2 fingers: zoom in around the center of the fingers.
2655 - (void) handlePinch:(UIPinchGestureRecognizer *)sender
2657 if (!xsft->event_cb || !xwindow) return;
2659 [self showCloseButton];
2661 if (sender.state == UIGestureRecognizerStateBegan)
2662 pinch_transform = self.transform; // Save the base transform
2664 switch (sender.state) {
2665 case UIGestureRecognizerStateBegan:
2666 case UIGestureRecognizerStateChanged:
2668 double scale = sender.scale;
2673 self.transform = CGAffineTransformScale (pinch_transform, scale, scale);
2675 CGPoint p = [sender locationInView: self];
2676 p.x /= self.layer.bounds.size.width;
2677 p.y /= self.layer.bounds.size.height;
2679 CGPoint np = CGPointMake (self.bounds.size.width * p.x,
2680 self.bounds.size.height * p.y);
2681 CGPoint op = CGPointMake (self.bounds.size.width *
2682 self.layer.anchorPoint.x,
2683 self.bounds.size.height *
2684 self.layer.anchorPoint.y);
2685 np = CGPointApplyAffineTransform (np, self.transform);
2686 op = CGPointApplyAffineTransform (op, self.transform);
2688 CGPoint pos = self.layer.position;
2693 self.layer.position = pos;
2694 self.layer.anchorPoint = p;
2698 case UIGestureRecognizerStateEnded:
2700 // When released, snap back to the default zoom (but animate it).
2702 CABasicAnimation *a1 = [CABasicAnimation
2703 animationWithKeyPath:@"position.x"];
2704 a1.fromValue = [NSNumber numberWithFloat: self.layer.position.x];
2705 a1.toValue = [NSNumber numberWithFloat: self.bounds.size.width / 2];
2707 CABasicAnimation *a2 = [CABasicAnimation
2708 animationWithKeyPath:@"position.y"];
2709 a2.fromValue = [NSNumber numberWithFloat: self.layer.position.y];
2710 a2.toValue = [NSNumber numberWithFloat: self.bounds.size.height / 2];
2712 CABasicAnimation *a3 = [CABasicAnimation
2713 animationWithKeyPath:@"anchorPoint.x"];
2714 a3.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.x];
2715 a3.toValue = [NSNumber numberWithFloat: 0.5];
2717 CABasicAnimation *a4 = [CABasicAnimation
2718 animationWithKeyPath:@"anchorPoint.y"];
2719 a4.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.y];
2720 a4.toValue = [NSNumber numberWithFloat: 0.5];
2722 CABasicAnimation *a5 = [CABasicAnimation
2723 animationWithKeyPath:@"transform.scale"];
2724 a5.fromValue = [NSNumber numberWithFloat: sender.scale];
2725 a5.toValue = [NSNumber numberWithFloat: 1.0];
2727 CAAnimationGroup *group = [CAAnimationGroup animation];
2728 group.duration = 0.3;
2729 group.repeatCount = 1;
2730 group.autoreverses = NO;
2731 group.animations = @[ a1, a2, a3, a4, a5 ];
2732 group.timingFunction = [CAMediaTimingFunction
2734 kCAMediaTimingFunctionEaseIn];
2735 [self.layer addAnimation:group forKey:@"unpinch"];
2737 self.transform = pinch_transform;
2738 self.layer.anchorPoint = CGPointMake (0.5, 0.5);
2739 self.layer.position = CGPointMake (self.bounds.size.width / 2,
2740 self.bounds.size.height / 2);
2749 /* We need this to respond to "shake" gestures
2751 - (BOOL)canBecomeFirstResponder
2756 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2761 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2765 /* Shake means exit and launch a new saver.
2767 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2769 [self stopAndClose:YES];
2773 - (void) showCloseButton
2780 int width = self.bounds.size.width;
2781 closeBox = [[UIView alloc]
2782 initWithFrame:CGRectMake(0, 0, width, ih + off)];
2783 closeBox.backgroundColor = [UIColor clearColor];
2784 closeBox.autoresizingMask =
2785 UIViewAutoresizingFlexibleBottomMargin |
2786 UIViewAutoresizingFlexibleWidth;
2788 // Add the buttons to the bar
2789 UIImage *img1 = [UIImage imageNamed:@"stop"];
2790 UIImage *img2 = [UIImage imageNamed:@"settings"];
2792 UIButton *button = [[UIButton alloc] init];
2793 [button setFrame: CGRectMake(off, off, iw, ih)];
2794 [button setBackgroundImage:img1 forState:UIControlStateNormal];
2795 [button addTarget:self
2796 action:@selector(stopAndClose)
2797 forControlEvents:UIControlEventTouchUpInside];
2798 [closeBox addSubview:button];
2801 button = [[UIButton alloc] init];
2802 [button setFrame: CGRectMake(width - iw - off, off, iw, ih)];
2803 [button setBackgroundImage:img2 forState:UIControlStateNormal];
2804 [button addTarget:self
2805 action:@selector(stopAndOpenSettings)
2806 forControlEvents:UIControlEventTouchUpInside];
2807 button.autoresizingMask =
2808 UIViewAutoresizingFlexibleBottomMargin |
2809 UIViewAutoresizingFlexibleLeftMargin;
2810 [closeBox addSubview:button];
2813 [self addSubview:closeBox];
2816 // Don't hide the buttons under the iPhone X bezel.
2817 UIEdgeInsets is = { 0, };
2818 if ([self respondsToSelector:@selector(safeAreaInsets)]) {
2819 # pragma clang diagnostic push // "only available on iOS 11.0 or newer"
2820 # pragma clang diagnostic ignored "-Wunguarded-availability-new"
2821 is = [self safeAreaInsets];
2822 # pragma clang diagnostic pop
2823 [closeBox setFrame:CGRectMake(is.left, is.top,
2824 self.bounds.size.width - is.right - is.left,
2828 if (closeBox.layer.opacity <= 0) { // Fade in
2830 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
2831 anim.duration = 0.2;
2832 anim.repeatCount = 1;
2833 anim.autoreverses = NO;
2834 anim.fromValue = [NSNumber numberWithFloat:0.0];
2835 anim.toValue = [NSNumber numberWithFloat:1.0];
2836 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
2837 closeBox.layer.opacity = 1;
2840 // Fade out N seconds from now.
2842 [closeBoxTimer invalidate];
2843 closeBoxTimer = [NSTimer scheduledTimerWithTimeInterval: 3
2845 selector:@selector(closeBoxOff)
2853 if (closeBoxTimer) {
2854 [closeBoxTimer invalidate];
2860 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
2861 anim.duration = 0.2;
2862 anim.repeatCount = 1;
2863 anim.autoreverses = NO;
2864 anim.fromValue = [NSNumber numberWithFloat: 1];
2865 anim.toValue = [NSNumber numberWithFloat: 0];
2866 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
2867 closeBox.layer.opacity = 0;
2871 - (void) stopAndOpenSettings
2873 NSString *s = [NSString stringWithCString:xsft->progclass
2874 encoding:NSISOLatin1StringEncoding];
2875 if ([self isAnimating])
2876 [self stopAnimation];
2877 [self resignFirstResponder];
2878 [_delegate wantsFadeOut:self];
2879 [_delegate openPreferences: s];
2884 - (void)setScreenLocked:(BOOL)locked
2886 if (screenLocked == locked) return;
2887 screenLocked = locked;
2889 if ([self isAnimating])
2890 [self stopAnimation];
2892 if (! [self isAnimating])
2893 [self startAnimation];
2897 - (NSDictionary *)getGLProperties
2899 return [NSDictionary dictionaryWithObjectsAndKeys:
2900 kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
2902 /* This could be disabled if we knew the screen would be redrawn
2903 entirely for every frame.
2905 [NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
2910 - (void)addExtraRenderbuffers:(CGSize)size
2912 // No extra renderbuffers are needed for 2D screenhacks.
2916 - (NSString *)getCAGravity
2918 return kCAGravityCenter; // Looks better in e.g. Compass.
2919 // return kCAGravityBottomLeft;
2922 #endif // USE_IPHONE
2925 - (void) checkForUpdates
2928 // We only check once at startup, even if there are multiple screens,
2929 // and even if this saver is running for many days.
2930 // (Uh, except this doesn't work because this static isn't shared,
2931 // even if we make it an exported global. Not sure why. Oh well.)
2932 static BOOL checked_p = NO;
2933 if (checked_p) return;
2936 // If it's off, don't bother running the updater. Otherwise, the
2937 // updater will decide if it's time to hit the network.
2938 if (! get_boolean_resource (xdpy,
2939 SUSUEnableAutomaticChecksKey,
2940 SUSUEnableAutomaticChecksKey))
2943 NSString *updater = @"XScreenSaverUpdater.app";
2945 // There may be multiple copies of the updater: e.g., one in /Applications
2946 // and one in the mounted installer DMG! It's important that we run the
2947 // one from the disk and not the DMG, so search for the right one.
2949 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2950 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2952 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2953 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2954 @"/Library/Screen Savers",
2955 @"/System/Library/Screen Savers",
2957 @"/Applications/Utilities"];
2958 NSString *app_path = nil;
2959 for (NSString *dir in search) {
2960 NSString *p = [dir stringByAppendingPathComponent:updater];
2961 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2968 app_path = [workspace fullPathForApplication:updater];
2970 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2971 app_path = 0; // The DMG version will not do.
2974 NSLog(@"Unable to find %@", updater);
2979 if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2980 options:(NSWorkspaceLaunchWithoutAddingToRecents |
2981 NSWorkspaceLaunchWithoutActivation |
2982 NSWorkspaceLaunchAndHide)
2983 configuration:[NSMutableDictionary dictionary]
2985 NSLog(@"Unable to launch %@: %@", app_path, err);
2988 # endif // !USE_IPHONE
2994 /* Utility functions...
2997 static PrefsReader *
2998 get_prefsReader (Display *dpy)
3000 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
3001 if (!view) return 0;
3002 return [view prefsReader];
3007 get_string_resource (Display *dpy, char *name, char *class)
3009 return [get_prefsReader(dpy) getStringResource:name];
3013 get_boolean_resource (Display *dpy, char *name, char *class)
3015 return [get_prefsReader(dpy) getBooleanResource:name];
3019 get_integer_resource (Display *dpy, char *name, char *class)
3021 return [get_prefsReader(dpy) getIntegerResource:name];
3025 get_float_resource (Display *dpy, char *name, char *class)
3027 return [get_prefsReader(dpy) getFloatResource:name];