1 /* xscreensaver, Copyright (c) 2006-2018 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 ();
452 - (id) initWithFrame:(NSRect)frame
453 saverName:(NSString *)saverName
454 isPreview:(BOOL)isPreview
455 isTouchbar:(BOOL)isTouchbar
457 if (! (self = [self initWithFrame:frame saverName:saverName
458 isPreview:isPreview]))
460 touchbar_p = isTouchbar;
463 #endif // USE_TOUCHBAR
469 return [CAEAGLLayer class];
474 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
476 return [self initWithFrame:frame saverName:0 isPreview:p];
482 if ([self isAnimating])
483 [self stopAnimation];
484 NSAssert(!xdata, @"xdata not yet freed");
485 NSAssert(!xdpy, @"xdpy not yet freed");
488 [[NSNotificationCenter defaultCenter] removeObserver:self];
491 # ifdef BACKBUFFER_OPENGL
494 # endif // !USE_IPHONE
496 // Releasing the OpenGL context should also free any OpenGL objects,
497 // including the backbuffer texture and frame/render/depthbuffers.
498 # endif // BACKBUFFER_OPENGL
500 # if defined JWXYZ_GL && defined USE_IPHONE
501 [ogl_ctx_pixmap release];
506 CGColorSpaceRelease (colorspace);
507 # endif // JWXYZ_QUARTZ
509 [prefsReader release];
517 - (PrefsReader *) prefsReader
524 - (void) lockFocus { }
525 - (void) unlockFocus { }
531 /* A few seconds after the saver launches, we store the "wasRunning"
532 preference. This is so that if the saver is crashing at startup,
533 we don't launch it again next time, getting stuck in a crash loop.
535 - (void) allSystemsGo: (NSTimer *) timer
537 NSAssert (timer == crash_timer, @"crash timer screwed up");
540 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
541 [prefs setBool:YES forKey:@"wasRunning"];
551 CGSize screen_size = self.bounds.size;
552 double s = self.contentScaleFactor;
553 screen_size.width *= s;
554 screen_size.height *= s;
557 GLuint *framebuffer = &xwindow->gl_framebuffer;
558 GLuint *renderbuffer = &xwindow->gl_renderbuffer;
559 xwindow->window.current_drawable = xwindow;
560 #elif defined JWXYZ_QUARTZ
561 GLuint *framebuffer = &gl_framebuffer;
562 GLuint *renderbuffer = &gl_renderbuffer;
563 #endif // JWXYZ_QUARTZ
565 if (*framebuffer) glDeleteFramebuffersOES (1, framebuffer);
566 if (*renderbuffer) glDeleteRenderbuffersOES (1, renderbuffer);
568 create_framebuffer (framebuffer, renderbuffer);
571 // glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES,
572 // (int)size.width, (int)size.height);
573 [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
574 fromDrawable:(CAEAGLLayer*)self.layer];
576 glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,
577 GL_RENDERBUFFER_OES, *renderbuffer);
579 [self addExtraRenderbuffers:screen_size];
581 check_framebuffer_status();
586 - (void) startAnimation
588 NSAssert(![self isAnimating], @"already animating");
589 NSAssert(!initted_p && !xdata, @"already initialized");
591 // See comment in render_x11() for why this value is important:
592 [self setAnimationTimeInterval: 1.0 / 240.0];
594 [super startAnimation];
595 /* We can't draw on the window from this method, so we actually do the
596 initialization of the screen saver (xsft->init_cb) in the first call
597 to animateOneFrame() instead.
602 [crash_timer invalidate];
604 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
605 [prefs removeObjectForKey:@"wasRunning"];
608 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
610 selector:@selector(allSystemsGo:)
614 # endif // USE_IPHONE
616 // Never automatically turn the screen off if we are docked,
617 // and an animation is running.
620 [UIApplication sharedApplication].idleTimerDisabled =
621 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
624 xwindow = (Window) calloc (1, sizeof(*xwindow));
625 xwindow->type = WINDOW;
626 xwindow->window.view = self;
627 CFRetain (xwindow->window.view); // needed for garbage collection?
629 #ifdef BACKBUFFER_OPENGL
630 CGSize new_backbuffer_size;
636 pixfmt = [self getGLPixelFormat];
639 NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
641 // Fun: On OS X 10.7, the second time an OpenGL context is created, after
642 // the preferences dialog is launched in SaverTester, the context only
643 // lasts until the first full GC. Then it turns black. Solution is to
644 // reuse the OpenGL context after this point.
645 // "Analyze" says that both pixfmt and ogl_ctx are leaked.
646 ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
649 // Sync refreshes to the vertical blanking interval
651 [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
652 // check_gl_error ("NSOpenGLCPSwapInterval"); // SEGV sometimes. Too early?
655 [ogl_ctx makeCurrentContext];
656 check_gl_error ("makeCurrentContext");
658 // NSOpenGLContext logs an 'invalid drawable' when this is called
659 // from initWithFrame.
660 [ogl_ctx setView:self];
662 // Get device pixels instead of points.
663 self.wantsBestResolutionOpenGLSurface = YES;
665 // This may not be necessary if there's FBO support.
667 xwindow->window.pixfmt = pixfmt;
668 CFRetain (xwindow->window.pixfmt);
669 xwindow->window.virtual_screen = [ogl_ctx currentVirtualScreen];
670 xwindow->window.current_drawable = xwindow;
671 NSAssert (ogl_ctx, @"no CGContext");
674 // Clear frame buffer ASAP, else there are bits left over from other apps.
675 glClearColor (0, 0, 0, 1);
676 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
678 // glXSwapBuffers (mi->dpy, mi->window);
681 // Enable multi-threading, if possible. This runs most OpenGL commands
682 // and GPU management on a second CPU.
684 # ifndef kCGLCEMPEngine
685 # define kCGLCEMPEngine 313 // Added in MacOS 10.4.8 + XCode 2.4.
687 CGLContextObj cctx = CGLGetCurrentContext();
688 CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
689 if (err != kCGLNoError) {
690 NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
694 new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
696 // Scale factor for desktop retina displays
697 double s = [self hackedContentScaleFactor];
698 new_backbuffer_size.width *= s;
699 new_backbuffer_size.height *= s;
703 CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
704 eagl_layer.opaque = TRUE;
705 eagl_layer.drawableProperties = [self getGLProperties];
707 // Without this, the GL frame buffer is half the screen resolution!
708 eagl_layer.contentsScale = [UIScreen mainScreen].scale;
710 ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
712 ogl_ctx_pixmap = [[EAGLContext alloc]
713 initWithAPI:kEAGLRenderingAPIOpenGLES1
714 sharegroup:ogl_ctx.sharegroup];
717 eagl_layer.contentsGravity = [self getCAGravity];
721 xwindow->window.ogl_ctx_pixmap = ogl_ctx_pixmap;
724 [EAGLContext setCurrentContext: ogl_ctx];
728 double s = [self hackedContentScaleFactor];
729 new_backbuffer_size = self.bounds.size;
730 new_backbuffer_size.width *= s;
731 new_backbuffer_size.height *= s;
733 # endif // USE_IPHONE
736 xwindow->ogl_ctx = ogl_ctx;
738 CFRetain (xwindow->ogl_ctx);
739 # endif // USE_IPHONE
742 check_gl_error ("startAnimation");
744 // NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
745 // glGetString (GL_RENDERER), glGetString (GL_VERSION));
747 [self enableBackbuffer:new_backbuffer_size];
749 #endif // BACKBUFFER_OPENGL
752 [self createBackbuffer:new_backbuffer_size];
755 if (touchbar_view) [touchbar_view startAnimation];
756 # endif // USE_TOUCHBAR
759 - (void)stopAnimation
761 NSAssert([self isAnimating], @"not animating");
765 [self lockFocus]; // in case something tries to draw from here
766 [self prepareContext];
768 /* All of the xlockmore hacks need to have their release functions
769 called, or launching the same saver twice does not work. Also
770 webcollage-cocoa needs it in order to kill the inferior webcollage
771 processes (since the screen saver framework never generates a
775 xsft->free_cb (xdpy, xwindow, xdata);
778 jwxyz_quartz_free_display (xdpy);
780 # if defined JWXYZ_GL && !defined USE_IPHONE
781 CFRelease (xwindow->ogl_ctx);
783 CFRelease (xwindow->window.view);
787 // setup_p = NO; // #### wait, do we need this?
794 [crash_timer invalidate];
796 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
797 [prefs removeObjectForKey:@"wasRunning"];
799 # endif // USE_IPHONE
801 [super stopAnimation];
803 // When an animation is no longer running (e.g., looking at the list)
804 // then it's ok to power off the screen when docked.
807 [UIApplication sharedApplication].idleTimerDisabled = NO;
810 // Without this, the GL frame stays on screen when switching tabs
811 // in System Preferences.
812 // (Or perhaps it used to. It doesn't seem to matter on 10.9.)
815 [NSOpenGLContext clearCurrentContext];
816 # endif // !USE_IPHONE
818 clear_gl_error(); // This hack is defunct, don't let this linger.
821 CGContextRelease (backbuffer);
825 munmap (backbuffer_data, backbuffer_len);
826 backbuffer_data = NULL;
832 [touchbar_view stopAnimation];
833 [touchbar_view release];
840 - (NSOpenGLContext *) oglContext
846 // #### maybe this could/should just be on 'lockFocus' instead?
847 - (void) prepareContext
851 [EAGLContext setCurrentContext:ogl_ctx];
853 [ogl_ctx makeCurrentContext];
854 // check_gl_error ("makeCurrentContext");
855 #endif // !USE_IPHONE
858 xwindow->window.current_drawable = xwindow;
866 static NSString *touchbar_cid = @"org.jwz.xscreensaver.touchbar";
867 static NSString *touchbar_iid = @"org.jwz.xscreensaver.touchbar";
869 - (NSTouchBar *) makeTouchBar
871 NSTouchBar *t = [[NSTouchBar alloc] init];
873 t.customizationIdentifier = touchbar_cid;
874 t.defaultItemIdentifiers = @[touchbar_iid,
875 NSTouchBarItemIdentifierOtherItemsProxy];
876 t.customizationAllowedItemIdentifiers = @[touchbar_iid];
877 t.principalItemIdentifier = touchbar_iid;
881 - (NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar
882 makeItemForIdentifier:(NSTouchBarItemIdentifier)id
884 if ([id isEqualToString:touchbar_iid])
886 NSRect rect = [self frame];
890 rect.size.width = 200;
891 rect.size.height = 40;
892 touchbar_view = [[[self class] alloc]
894 saverName:[NSString stringWithCString:xsft->progclass
895 encoding:NSISOLatin1StringEncoding]
896 isPreview:self.isPreview
898 [touchbar_view setAutoresizingMask:
899 NSViewWidthSizable|NSViewHeightSizable];
900 NSCustomTouchBarItem *item =
901 [[NSCustomTouchBarItem alloc] initWithIdentifier:id];
902 item.view = touchbar_view;
903 item.customizationLabel = touchbar_cid;
905 if ([self isAnimating])
906 // TouchBar was created after animation begun.
907 [touchbar_view startAnimation];
912 #endif // USE_TOUCHBAR
916 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
918 fps_compute (fpst, 0, -1);
923 /* Some of the older X11 savers look bad if a "pixel" is not a thing you can
924 see. They expect big, chunky, luxurious 1990s pixels, and if they use
925 "device" pixels on a Retina screen, everything just disappears.
927 Retina iPads have 768x1024 point screens which are 1536x2048 pixels,
928 2017 iMac screens are 5120x2880 in device pixels.
930 This method is overridden in XScreenSaverGLView, since this kludge
931 isn't necessary for GL programs, being resolution independent by
934 - (CGFloat) hackedContentScaleFactor
937 CGFloat s = self.contentScaleFactor;
939 CGFloat s = self.window.backingScaleFactor;
943 NSSize b = [self bounds].size;
944 CGFloat wh = b.width > b.height ? b.width : b.height;
946 // Scale down to as close to 1024 as we can get without going under,
947 // while keeping an integral scale factor so that we don't get banding
948 // artifacts and moire patterns.
950 // Retina sizes: 2208 => 1104, 2224 => 1112, 2732 => 1366, 2880 => 1440.
963 current_device_rotation (void)
965 UIDeviceOrientation o = [[UIDevice currentDevice] orientation];
967 /* Sometimes UIDevice doesn't know the proper orientation, or the device is
968 face up/face down, so in those cases fall back to the status bar
969 orientation. The SaverViewController tries to set the status bar to the
970 proper orientation before it creates the XScreenSaverView; see
971 _storedOrientation in SaverViewController.
973 if (o == UIDeviceOrientationUnknown ||
974 o == UIDeviceOrientationFaceUp ||
975 o == UIDeviceOrientationFaceDown) {
976 /* Mind the differences between UIInterfaceOrientation and
978 1. UIInterfaceOrientation does not include FaceUp and FaceDown.
979 2. LandscapeLeft and LandscapeRight are swapped between the two. But
980 converting between device and interface orientation doesn't need to
981 take this into account, because (from the UIInterfaceOrientation
982 description): "rotating the device requires rotating the content in
983 the opposite direction."
985 /* statusBarOrientation deprecated in iOS 9 */
986 o = (UIDeviceOrientation) // from UIInterfaceOrientation
987 [UIApplication sharedApplication].statusBarOrientation;
991 case UIDeviceOrientationLandscapeLeft: return -90; break;
992 case UIDeviceOrientationLandscapeRight: return 90; break;
993 case UIDeviceOrientationPortraitUpsideDown: return 180; break;
994 default: return 0; break;
999 - (void) handleException: (NSException *)e
1001 NSLog (@"Caught exception: %@", e);
1002 UIAlertController *c = [UIAlertController
1003 alertControllerWithTitle:
1004 [NSString stringWithFormat: @"%s crashed!",
1006 message: [NSString stringWithFormat:
1007 @"The error message was:"
1009 "If it keeps crashing, try "
1010 "resetting its options.",
1012 preferredStyle:UIAlertControllerStyleAlert];
1014 [c addAction: [UIAlertAction actionWithTitle: @"Exit"
1015 style: UIAlertActionStyleDefault
1016 handler: ^(UIAlertAction *a) {
1019 [c addAction: [UIAlertAction actionWithTitle: @"Keep going"
1020 style: UIAlertActionStyleDefault
1021 handler: ^(UIAlertAction *a) {
1022 [self stopAndClose:NO];
1025 UIViewController *vc =
1026 [UIApplication sharedApplication].keyWindow.rootViewController;
1027 while (vc.presentedViewController)
1028 vc = vc.presentedViewController;
1029 [vc presentViewController:c animated:YES completion:nil];
1030 [self stopAnimation];
1033 #endif // USE_IPHONE
1042 // iOS always uses OpenGL ES 1.1.
1048 gl_check_ver (const struct gl_version *caps,
1052 return caps->major > gl_major ||
1053 (caps->major == gl_major && caps->minor >= gl_minor);
1058 /* Called during startAnimation before the first call to createBackbuffer. */
1059 - (void) enableBackbuffer:(CGSize)new_backbuffer_size
1062 struct gl_version version;
1065 const char *version_str = (const char *)glGetString (GL_VERSION);
1067 /* iPhone is always OpenGL ES 1.1. */
1068 if (sscanf ((const char *)version_str, "%u.%u",
1069 &version.major, &version.minor) < 2)
1077 // The OpenGL extensions in use in here are pretty are pretty much ubiquitous
1078 // on OS X, but it's still good form to check.
1079 const GLubyte *extensions = glGetString (GL_EXTENSIONS);
1081 glGenTextures (1, &backbuffer_texture);
1083 // On really old systems, it would make sense to split the texture
1086 gl_texture_target = (gluCheckExtension ((const GLubyte *)
1087 "GL_ARB_texture_rectangle",
1089 ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
1091 // OES_texture_npot also provides this, but iOS never provides it.
1092 gl_limited_npot_p = jwzgles_gluCheckExtension
1093 ((const GLubyte *) "GL_APPLE_texture_2D_limited_npot", extensions);
1094 gl_texture_target = GL_TEXTURE_2D;
1097 glBindTexture (gl_texture_target, backbuffer_texture);
1098 glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1099 // GL_LINEAR might make sense on Retina iPads.
1100 glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1101 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1102 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1105 // There isn't much sense in supporting one of these if the other
1107 gl_apple_client_storage_p =
1108 gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage",
1110 gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions);
1112 if (gl_apple_client_storage_p) {
1113 glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE,
1114 GL_STORAGE_SHARED_APPLE);
1115 glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
1119 // If a video adapter suports BGRA textures, then that's probably as fast as
1120 // you're gonna get for getting a texture onto the screen.
1123 jwzgles_gluCheckExtension
1124 ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888", extensions) ?
1128 gl_pixel_type = GL_UNSIGNED_BYTE;
1129 // See also OES_read_format.
1131 if (gl_check_ver (&version, 1, 2) ||
1132 (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) &&
1133 gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels",
1135 gl_pixel_format = GL_BGRA;
1136 // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV.
1137 gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1139 gl_pixel_format = GL_RGBA;
1140 gl_pixel_type = GL_UNSIGNED_BYTE;
1142 // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more
1143 // sense on PowerPC.
1146 glEnable (gl_texture_target);
1147 glEnableClientState (GL_VERTEX_ARRAY);
1148 glEnableClientState (GL_TEXTURE_COORD_ARRAY);
1150 check_gl_error ("enableBackbuffer");
1155 - (BOOL) suppressRotationAnimation
1157 return [self ignoreRotation]; // Don't animate if we aren't rotating
1160 - (BOOL) rotateTouches
1162 return FALSE; // Adjust event coordinates only if rotating
1167 - (void) setViewport
1169 # ifdef BACKBUFFER_OPENGL
1170 NSAssert ([NSOpenGLContext currentContext] ==
1171 ogl_ctx, @"invalid GL context");
1173 NSSize new_size = self.bounds.size;
1176 GLfloat s = self.contentScaleFactor;
1177 # else // !USE_IPHONE
1178 const GLfloat s = self.window.backingScaleFactor;
1180 GLfloat hs = self.hackedContentScaleFactor;
1182 // On OS X this almost isn't necessary, except for the ugly aliasing
1184 glViewport (0, 0, new_size.width * s, new_size.height * s);
1186 glMatrixMode (GL_PROJECTION);
1193 (-new_size.width * hs, new_size.width * hs,
1194 -new_size.height * hs, new_size.height * hs,
1198 if ([self ignoreRotation]) {
1199 int o = (int) -current_device_rotation();
1200 glRotatef (o, 0, 0, 1);
1202 # endif // USE_IPHONE
1203 # endif // BACKBUFFER_OPENGL
1207 /* Create a bitmap context into which we render everything.
1208 If the desired size has changed, re-created it.
1209 new_size is in rotated pixels, not points: the same size
1210 and shape as the X11 window as seen by the hacks.
1212 - (void) createBackbuffer:(CGSize)new_size
1214 CGSize osize = CGSizeZero;
1216 osize.width = CGBitmapContextGetWidth(backbuffer);
1217 osize.height = CGBitmapContextGetHeight(backbuffer);
1221 (int)osize.width == (int)new_size.width &&
1222 (int)osize.height == (int)new_size.height)
1225 CGContextRef ob = backbuffer;
1226 void *odata = backbuffer_data;
1227 GLsizei olen = backbuffer_len;
1229 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1230 NSLog(@"backbuffer %.0fx%.0f",
1231 new_size.width, new_size.height);
1234 /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
1235 <https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html>.
1237 iOS uses bog-standard glTexImage2D (for now).
1239 glMapBuffer is the standard way to get data from system RAM to video
1240 memory asynchronously and without a memcpy, but support for
1241 APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer),
1242 and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3
1243 (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with
1244 CGBitmapContext: glMapBuffer can return a different pointer on each
1245 call, but a CGBitmapContext doesn't allow its data pointer to be
1246 changed -- and recreating the context for a new pointer can be
1247 expensive (glyph caches get dumped, for instance).
1249 glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT,
1250 and these seem to allow mapping the buffer and leaving it where it is
1251 in client address space while OpenGL works with the buffer, but it
1252 requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for
1253 GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage.
1255 AMD_pinned_buffer provides the same advantage as glMapBufferRange, but
1256 Apple never implemented that one for OS X.
1259 backbuffer_data = NULL;
1260 gl_texture_w = (int)new_size.width;
1261 gl_texture_h = (int)new_size.height;
1263 NSAssert (gl_texture_target == GL_TEXTURE_2D
1265 || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT
1267 , @"unexpected GL texture target");
1270 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1272 if (!gl_limited_npot_p)
1275 gl_texture_w = (GLsizei) to_pow2 (gl_texture_w);
1276 gl_texture_h = (GLsizei) to_pow2 (gl_texture_h);
1279 GLsizei bytes_per_row = gl_texture_w * 4;
1281 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1282 // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
1283 // it will fall back to a memcpy.
1284 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24
1285 bytes_per_row = (bytes_per_row + 31) & ~31;
1286 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1288 backbuffer_len = bytes_per_row * gl_texture_h;
1289 if (backbuffer_len) // mmap requires this to be non-zero.
1290 backbuffer_data = mmap (NULL, backbuffer_len,
1291 PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED,
1294 BOOL alpha_first_p, order_little_p;
1296 if (gl_pixel_format == GL_BGRA) {
1297 alpha_first_p = YES;
1298 order_little_p = YES;
1300 } else if (gl_pixel_format == GL_ABGR_EXT) {
1302 order_little_p = YES; */
1304 NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format");
1306 order_little_p = NO;
1310 NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type");
1312 NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 ||
1313 gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV ||
1314 gl_pixel_type == GL_UNSIGNED_BYTE,
1315 @"unknown GL pixel type");
1317 #if defined __LITTLE_ENDIAN__
1318 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8;
1319 #elif defined __BIG_ENDIAN__
1320 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1322 # error Unknown byte order.
1325 if (gl_pixel_type == backwards_pixel_type)
1326 order_little_p ^= YES;
1329 CGBitmapInfo bitmap_info =
1330 (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) |
1331 (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
1333 backbuffer = CGBitmapContextCreate (backbuffer_data,
1334 (int)new_size.width,
1335 (int)new_size.height,
1340 NSAssert (backbuffer, @"unable to allocate back buffer");
1344 r.origin.x = r.origin.y = 0;
1346 CGContextSetGrayFillColor (backbuffer, 0, 1);
1347 CGContextFillRect (backbuffer, r);
1349 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1350 if (gl_apple_client_storage_p)
1351 glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data);
1352 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1355 // Restore old bits, as much as possible, to the X11 upper left origin.
1357 CGRect rect; // pixels, not points
1359 rect.origin.y = (new_size.height - osize.height);
1362 CGImageRef img = CGBitmapContextCreateImage (ob);
1363 CGContextDrawImage (backbuffer, rect, img);
1364 CGImageRelease (img);
1365 CGContextRelease (ob);
1368 // munmap should round len up to the nearest page.
1369 munmap (odata, olen);
1372 check_gl_error ("createBackbuffer");
1376 - (void) drawBackbuffer
1378 # ifdef BACKBUFFER_OPENGL
1380 NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]],
1381 @"ogl_ctx is not an NSOpenGLContext");
1383 NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4),
1384 @"improperly-aligned backbuffer");
1386 // This gets width and height from the backbuffer in case
1387 // APPLE_client_storage is in use. See the note in createBackbuffer.
1388 // This still has to happen every frame even when APPLE_client_storage has
1389 // the video adapter pulling texture data straight from
1390 // XScreenSaverView-owned memory.
1391 glTexImage2D (gl_texture_target, 0, GL_RGBA,
1392 (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4),
1393 gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
1396 GLfloat w = xwindow->frame.width, h = xwindow->frame.height;
1398 GLfloat vertices[4][2] = {{-w, h}, {w, h}, {w, -h}, {-w, -h}};
1400 GLfloat tex_coords[4][2];
1403 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1404 # endif // USE_IPHONE
1410 tex_coords[0][0] = 0;
1411 tex_coords[0][1] = 0;
1412 tex_coords[1][0] = w;
1413 tex_coords[1][1] = 0;
1414 tex_coords[2][0] = w;
1415 tex_coords[2][1] = h;
1416 tex_coords[3][0] = 0;
1417 tex_coords[3][1] = h;
1419 glVertexPointer (2, GL_FLOAT, 0, vertices);
1420 glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
1421 glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
1423 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1424 check_gl_error ("drawBackbuffer");
1426 # endif // BACKBUFFER_OPENGL
1429 #endif // JWXYZ_QUARTZ
1433 - (void)enableBackbuffer:(CGSize)new_backbuffer_size;
1435 jwxyz_set_matrices (new_backbuffer_size.width, new_backbuffer_size.height);
1436 check_gl_error ("enableBackbuffer");
1439 - (void)createBackbuffer:(CGSize)new_size
1441 NSAssert ([NSOpenGLContext currentContext] ==
1442 ogl_ctx, @"invalid GL context");
1443 NSAssert (xwindow->window.current_drawable == xwindow,
1444 @"current_drawable not set properly");
1447 /* On iOS, Retina means glViewport gets called with the screen size instead
1448 of the backbuffer/xwindow size. This happens in startAnimation.
1450 The GL screenhacks call glViewport themselves.
1452 glViewport (0, 0, new_size.width, new_size.height);
1455 // TODO: Preserve contents on resize.
1456 glClear (GL_COLOR_BUFFER_BIT);
1457 check_gl_error ("createBackbuffer");
1463 - (void)flushBackbuffer
1466 // Make sure the right context is active: there's two under JWXYZ_GL.
1467 jwxyz_bind_drawable (xwindow, xwindow);
1472 # ifdef JWXYZ_QUARTZ
1473 // The OpenGL pipeline is not automatically synchronized with the contents
1474 // of the backbuffer, so without glFinish, OpenGL can start rendering from
1475 // the backbuffer texture at the same time that JWXYZ is clearing and
1476 // drawing the next frame in the backing store for the backbuffer texture.
1477 // This is only a concern under JWXYZ_QUARTZ because of
1478 // APPLE_client_storage; JWXYZ_GL doesn't use that.
1480 # endif // JWXYZ_QUARTZ
1482 // If JWXYZ_GL was single-buffered, there would need to be a glFinish (or
1483 // maybe just glFlush?) here, because single-buffered contexts don't always
1484 // update what's on the screen after drawing finishes. (i.e., in safe mode)
1486 # ifdef JWXYZ_QUARTZ
1487 // JWXYZ_GL is always double-buffered.
1488 if (double_buffered_p)
1489 # endif // JWXYZ_QUARTZ
1490 [ogl_ctx flushBuffer]; // despite name, this actually swaps
1491 # else // USE_IPHONE
1493 // jwxyz_bind_drawable() only binds the framebuffer, not the renderbuffer.
1495 GLint gl_renderbuffer = xwindow->gl_renderbuffer;
1498 glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
1499 [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
1500 # endif // USE_IPHONE
1502 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1503 // glGetError waits for the OpenGL command pipe to flush, so skip it in
1505 // OpenGL Programming Guide for Mac -> OpenGL Application Design
1506 // Strategies -> Allow OpenGL to Manage Your Resources
1507 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7
1508 check_gl_error ("flushBackbuffer");
1513 /* Inform X11 that the size of our window has changed.
1517 if (!xdpy) return; // early
1519 NSSize new_size; // pixels, not points
1521 new_size = self.bounds.size;
1525 // If this hack ignores rotation, then that means that it pretends to
1526 // always be in portrait mode. If the View has been resized to a
1527 // landscape shape, swap width and height to keep the backbuffer
1530 double rot = current_device_rotation();
1531 if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
1532 CGFloat swap = new_size.width;
1533 new_size.width = new_size.height;
1534 new_size.height = swap;
1536 # endif // USE_IPHONE
1538 double s = self.hackedContentScaleFactor;
1539 new_size.width *= s;
1540 new_size.height *= s;
1542 [self prepareContext];
1545 // On first resize, xwindow->frame is 0x0.
1546 if (xwindow->frame.width == new_size.width &&
1547 xwindow->frame.height == new_size.height)
1550 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1552 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1554 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
1555 xwindow->frame.x = 0;
1556 xwindow->frame.y = 0;
1557 xwindow->frame.width = new_size.width;
1558 xwindow->frame.height = new_size.height;
1560 [self createBackbuffer:CGSizeMake(xwindow->frame.width,
1561 xwindow->frame.height)];
1563 # if defined JWXYZ_QUARTZ
1564 xwindow->cgc = backbuffer;
1565 NSAssert (xwindow->cgc, @"no CGContext");
1566 # elif defined JWXYZ_GL && !defined USE_IPHONE
1568 [ogl_ctx setView:xwindow->window.view]; // (Is this necessary?)
1569 # endif // JWXYZ_GL && USE_IPHONE
1571 jwxyz_window_resized (xdpy);
1573 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1574 NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
1577 // Next time render_x11 is called, run the saver's reshape_cb.
1584 /* Called by SaverRunner when the device has changed orientation.
1585 That means we need to generate a resize event, even if the size
1586 has not changed (e.g., from LandscapeLeft to LandscapeRight).
1588 - (void) orientationChanged
1592 next_frame_time = 0; // Get a new frame on screen quickly
1595 /* A hook run after the 'reshape_' method has been called. Used by
1596 XScreenSaverGLView to adjust the in-scene GL viewport.
1598 - (void) postReshape
1601 #endif // USE_IPHONE
1604 // Only render_x11 should call this. XScreenSaverGLView specializes it.
1605 - (void) reshape_x11
1607 xsft->reshape_cb (xdpy, xwindow, xdata,
1608 xwindow->frame.width, xwindow->frame.height);
1617 // jwxyz_make_display needs this.
1618 [self prepareContext]; // resize_x11 also calls this.
1625 # ifdef JWXYZ_QUARTZ
1626 xwindow->cgc = backbuffer;
1627 # endif // JWXYZ_QUARTZ
1628 xdpy = jwxyz_quartz_make_display (xwindow);
1630 # if defined USE_IPHONE
1631 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
1634 TRUE; // Rotation doesn't work yet. TODO: Make rotation work.
1636 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
1637 # endif // !JWXYZ_GL
1638 # endif // USE_IPHONE
1640 _lowrez_p = get_boolean_resource (xdpy, "lowrez", "Lowrez");
1644 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1645 NSSize b = [self bounds].size;
1646 CGFloat s = self.hackedContentScaleFactor;
1648 CGFloat o = self.contentScaleFactor;
1650 CGFloat o = self.window.backingScaleFactor;
1653 NSLog(@"lowrez: scaling %.0fx%.0f -> %.0fx%.0f (%.02f)",
1654 b.width * o, b.height * o,
1655 b.width * s, b.height * s, s);
1665 xsft->setup_cb (xsft, xsft->setup_arg);
1668 NSAssert(!xdata, @"xdata already initialized");
1671 # undef ya_rand_init
1674 XSetWindowBackground (xdpy, xwindow,
1675 get_pixel_resource (xdpy, 0,
1676 "background", "Background"));
1677 XClearWindow (xdpy, xwindow);
1680 [[self window] setAcceptsMouseMovedEvents:YES];
1683 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
1684 drawing primitives will run on the GPU instead of the CPU.
1685 It seems like it might make things worse rather than better,
1686 though... Plus it makes us binary-incompatible with 10.4.
1688 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
1689 [[self window] setPreferredBackingLocation:
1690 NSWindowBackingLocationVideoMemory];
1694 /* Kludge: even though the init_cb functions are declared to take 2 args,
1695 actually call them with 3, for the benefit of xlockmore_init() and
1698 void *(*init_cb) (Display *, Window, void *) =
1699 (void *(*) (Display *, Window, void *)) xsft->init_cb;
1701 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
1702 // NSAssert(xdata, @"no xdata from init");
1703 if (! xdata) abort();
1705 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1706 fpst = fps_init (xdpy, xwindow);
1707 fps_cb = xsft->fps_cb;
1708 if (! fps_cb) fps_cb = screenhack_do_fps;
1715 if (current_device_rotation() != 0) // launched while rotated
1719 [self checkForUpdates];
1723 /* I don't understand why we have to do this *every frame*, but we do,
1724 or else the cursor comes back on.
1727 if (![self isPreview])
1728 [NSCursor setHiddenUntilMouseMoves:YES];
1734 /* This is just a guess, but the -fps code wants to know how long
1735 we were sleeping between frames.
1737 long usecs = 1000000 * [self animationTimeInterval];
1738 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
1739 if (usecs < 0) usecs = 0;
1740 fps_slept (fpst, usecs);
1744 /* Run any XtAppAddInput and XtAppAddTimeOut callbacks now.
1745 Do this before delaying for next_frame_time to avoid throttling
1746 timers to the hack's frame rate.
1748 XtAppProcessEvent (XtDisplayToApplicationContext (xdpy),
1749 XtIMTimer | XtIMAlternateInput);
1752 /* It turns out that on some systems (possibly only 10.5 and older?)
1753 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
1754 that we cannot rely on it.
1756 Some of the screen hacks want to delay for long periods, and letting the
1757 framework run the update function at 30 FPS when it really wanted half a
1758 minute between frames would be bad. So instead, we assume that the
1759 framework's animation timer might fire whenever, but we only invoke the
1760 screen hack's "draw frame" method when enough time has expired.
1762 This means two extra calls to gettimeofday() per frame. For fast-cycling
1763 screen savers, that might actually slow them down. Oh well.
1765 A side-effect of this is that it's not possible for a saver to request
1766 an animation interval that is faster than animationTimeInterval.
1768 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
1769 ignored, it's important that it be faster than 30 FPS. 240 FPS is good.
1771 An NSTimer won't fire if the timer is already running the invocation
1772 function from a previous firing. So, if we use a 30 FPS
1773 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
1774 frame, there will be a 26666 µs delay until the next frame, 66666 µs
1775 after the beginning of the current frame. In other words, 25 FPS
1778 Frame rates tend to snap to values of 30/N, where N is a positive
1779 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1780 is rounded down from what it would normally be.
1782 So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates
1783 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1784 steps for higher or lower animation time intervals respectively.
1787 gettimeofday (&tv, 0);
1788 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1789 if (now < next_frame_time) return;
1791 // [self flushBackbuffer];
1794 // We do this here instead of in setFrame so that all the
1795 // Xlib drawing takes place under the animation timer.
1799 [ogl_ctx setView:self];
1800 # endif // !USE_IPHONE
1809 // NSAssert(xdata, @"no xdata when drawing");
1810 if (! xdata) abort();
1811 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1813 fps_cb (xdpy, xwindow, fpst, xdata);
1815 gettimeofday (&tv, 0);
1816 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1817 next_frame_time = now + (delay / 1000000.0);
1819 # ifdef JWXYZ_QUARTZ
1820 [self drawBackbuffer];
1822 // This can also happen near the beginning of render_x11.
1823 [self flushBackbuffer];
1825 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1826 if (delay < [self animationTimeInterval])
1827 [self setAnimationTimeInterval:(delay / 1000000.0)];
1830 # ifdef DO_GC_HACKERY
1831 /* Current theory is that the 10.6 garbage collector sucks in the
1834 It only does a collection when a threshold of outstanding
1835 collectable allocations has been surpassed. However, CoreGraphics
1836 creates lots of small collectable allocations that contain pointers
1837 to very large non-collectable allocations: a small CG object that's
1838 collectable referencing large malloc'd allocations (non-collectable)
1839 containing bitmap data. So the large allocation doesn't get freed
1840 until GC collects the small allocation, which triggers its finalizer
1841 to run which frees the large allocation. So GC is deciding that it
1842 doesn't really need to run, even though the process has gotten
1843 enormous. GC eventually runs once pageouts have happened, but by
1844 then it's too late, and the machine's resident set has been
1847 So, we force an exhaustive garbage collection in this process
1848 approximately every 5 seconds whether the system thinks it needs
1852 static int tick = 0;
1853 if (++tick > 5*30) {
1855 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1858 # endif // DO_GC_HACKERY
1862 @catch (NSException *e) {
1863 [self handleException: e];
1865 # endif // USE_IPHONE
1869 - (void) animateOneFrame
1871 // Render X11 into the backing store bitmap...
1873 # ifdef USE_TOUCHBAR
1874 if (touchbar_p) return;
1877 # ifdef JWXYZ_QUARTZ
1878 NSAssert (backbuffer, @"no back buffer");
1881 UIGraphicsPushContext (backbuffer);
1883 # endif // JWXYZ_QUARTZ
1887 # if defined USE_IPHONE && defined JWXYZ_QUARTZ
1888 UIGraphicsPopContext();
1891 # ifdef USE_TOUCHBAR
1892 if (touchbar_view) [touchbar_view animateOneFrame];
1897 # ifndef USE_IPHONE // Doesn't exist on iOS
1899 - (void) setFrame:(NSRect) newRect
1901 [super setFrame:newRect];
1903 if (xwindow) // inform Xlib that the window has changed now.
1907 - (void) setFrameSize:(NSSize) newSize
1909 [super setFrameSize:newSize];
1914 # else // USE_IPHONE
1916 - (void) layoutSubviews
1918 [super layoutSubviews];
1927 +(BOOL) performGammaFade
1932 - (BOOL) hasConfigureSheet
1937 + (NSString *) decompressXML: (NSData *)data
1939 if (! data) return 0;
1940 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1942 // If it's not already XML, decompress it.
1943 NSAssert (compressed_p, @"xml isn't compressed");
1945 NSMutableData *data2 = 0;
1948 memset (&zs, 0, sizeof(zs));
1949 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1951 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1952 data2 = [NSMutableData dataWithLength: usize];
1953 zs.next_in = (Bytef *) data.bytes;
1954 zs.avail_in = (uint) data.length;
1955 zs.next_out = (Bytef *) data2.bytes;
1956 zs.avail_out = (uint) data2.length;
1957 ret = inflate (&zs, Z_FINISH);
1960 if (ret == Z_OK || ret == Z_STREAM_END)
1963 NSAssert2 (0, @"gunzip error: %d: %s",
1964 ret, (zs.msg ? zs.msg : "<null>"));
1967 NSString *s = [[NSString alloc]
1968 initWithData:data encoding:NSUTF8StringEncoding];
1975 - (NSWindow *) configureSheet
1977 - (UIViewController *) configureView
1980 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1981 NSString *file = [NSString stringWithCString:xsft->progclass
1982 encoding:NSISOLatin1StringEncoding];
1983 file = [file lowercaseString];
1984 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1986 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1987 file, [bundle resourcePath]);
1992 UIViewController *sheet;
1993 # else // !USE_IPHONE
1995 # endif // !USE_IPHONE
1997 NSData *xmld = [NSData dataWithContentsOfFile:path];
1998 NSString *xml = [[self class] decompressXML: xmld];
1999 sheet = [[XScreenSaverConfigSheet alloc]
2000 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
2001 options:xsft->options
2002 controller:[prefsReader userDefaultsController]
2003 globalController:[prefsReader globalDefaultsController]
2004 defaults:[prefsReader defaultOptions]];
2006 // #### am I expected to retain this, or not? wtf.
2007 // I thought not, but if I don't do this, we (sometimes) crash.
2008 // #### Analyze says "potential leak of an object stored into sheet"
2015 - (NSUserDefaultsController *) userDefaultsController
2017 return [prefsReader userDefaultsController];
2021 /* Announce our willingness to accept keyboard input.
2023 - (BOOL)acceptsFirstResponder
2033 # else // USE_IPHONE
2035 // There's no way to play a standard system alert sound!
2036 // We'd have to include our own WAV for that.
2038 // Or we could vibrate:
2039 // #import <AudioToolbox/AudioToolbox.h>
2040 // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
2042 // Instead, just flash the screen white, then fade.
2044 UIView *v = [[UIView alloc] initWithFrame: [self frame]];
2045 [v setBackgroundColor: [UIColor whiteColor]];
2046 [[self window] addSubview:v];
2047 [UIView animateWithDuration: 0.1
2048 animations:^{ [v setAlpha: 0.0]; }
2049 completion:^(BOOL finished) { [v removeFromSuperview]; } ];
2051 # endif // USE_IPHONE
2055 /* Send an XEvent to the hack. Returns YES if it was handled.
2057 - (BOOL) sendEvent: (XEvent *) e
2059 if (!initted_p || ![self isAnimating]) // no event handling unless running.
2063 [self prepareContext];
2064 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
2072 /* Convert an NSEvent into an XEvent, and pass it along.
2073 Returns YES if it was handled.
2075 - (BOOL) convertEvent: (NSEvent *) e
2079 memset (&xe, 0, sizeof(xe));
2083 int flags = [e modifierFlags];
2084 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
2085 if (flags & NSShiftKeyMask) state |= ShiftMask;
2086 if (flags & NSControlKeyMask) state |= ControlMask;
2087 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
2088 if (flags & NSCommandKeyMask) state |= Mod2Mask;
2090 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
2092 double s = [self hackedContentScaleFactor];
2094 int y = s * ([self bounds].size.height - p.y);
2096 xe.xany.type = type;
2102 xe.xbutton.state = state;
2103 if ([e type] == NSScrollWheel)
2104 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
2105 [e deltaY] < 0 ? Button5 :
2106 [e deltaX] > 0 ? Button6 :
2107 [e deltaX] < 0 ? Button7 :
2110 xe.xbutton.button = (unsigned int) [e buttonNumber] + 1;
2115 xe.xmotion.state = state;
2120 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
2121 [e charactersIgnoringModifiers]);
2124 if (!ns || [ns length] == 0) // dead key
2126 // Cocoa hides the difference between left and right keys.
2127 // Also we only get KeyPress events for these, no KeyRelease
2128 // (unless we hack the mod state manually. Bleh.)
2130 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
2131 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
2132 else if (flags & NSControlKeyMask) k = XK_Control_L;
2133 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
2134 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
2136 else if ([ns length] == 1) // real key
2138 switch ([ns characterAtIndex:0]) {
2139 case NSLeftArrowFunctionKey: k = XK_Left; break;
2140 case NSRightArrowFunctionKey: k = XK_Right; break;
2141 case NSUpArrowFunctionKey: k = XK_Up; break;
2142 case NSDownArrowFunctionKey: k = XK_Down; break;
2143 case NSPageUpFunctionKey: k = XK_Page_Up; break;
2144 case NSPageDownFunctionKey: k = XK_Page_Down; break;
2145 case NSHomeFunctionKey: k = XK_Home; break;
2146 case NSPrevFunctionKey: k = XK_Prior; break;
2147 case NSNextFunctionKey: k = XK_Next; break;
2148 case NSBeginFunctionKey: k = XK_Begin; break;
2149 case NSEndFunctionKey: k = XK_End; break;
2150 case NSF1FunctionKey: k = XK_F1; break;
2151 case NSF2FunctionKey: k = XK_F2; break;
2152 case NSF3FunctionKey: k = XK_F3; break;
2153 case NSF4FunctionKey: k = XK_F4; break;
2154 case NSF5FunctionKey: k = XK_F5; break;
2155 case NSF6FunctionKey: k = XK_F6; break;
2156 case NSF7FunctionKey: k = XK_F7; break;
2157 case NSF8FunctionKey: k = XK_F8; break;
2158 case NSF9FunctionKey: k = XK_F9; break;
2159 case NSF10FunctionKey: k = XK_F10; break;
2160 case NSF11FunctionKey: k = XK_F11; break;
2161 case NSF12FunctionKey: k = XK_F12; break;
2165 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
2166 k = (ss && *ss ? *ss : 0);
2172 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
2174 xe.xkey.keycode = k;
2175 xe.xkey.state = state;
2179 NSAssert1 (0, @"unknown X11 event type: %d", type);
2183 return [self sendEvent: &xe];
2187 - (void) mouseDown: (NSEvent *) e
2189 if (! [self convertEvent:e type:ButtonPress])
2190 [super mouseDown:e];
2193 - (void) mouseUp: (NSEvent *) e
2195 if (! [self convertEvent:e type:ButtonRelease])
2199 - (void) otherMouseDown: (NSEvent *) e
2201 if (! [self convertEvent:e type:ButtonPress])
2202 [super otherMouseDown:e];
2205 - (void) otherMouseUp: (NSEvent *) e
2207 if (! [self convertEvent:e type:ButtonRelease])
2208 [super otherMouseUp:e];
2211 - (void) mouseMoved: (NSEvent *) e
2213 if (! [self convertEvent:e type:MotionNotify])
2214 [super mouseMoved:e];
2217 - (void) mouseDragged: (NSEvent *) e
2219 if (! [self convertEvent:e type:MotionNotify])
2220 [super mouseDragged:e];
2223 - (void) otherMouseDragged: (NSEvent *) e
2225 if (! [self convertEvent:e type:MotionNotify])
2226 [super otherMouseDragged:e];
2229 - (void) scrollWheel: (NSEvent *) e
2231 if (! [self convertEvent:e type:ButtonPress])
2232 [super scrollWheel:e];
2235 - (void) keyDown: (NSEvent *) e
2237 if (! [self convertEvent:e type:KeyPress])
2241 - (void) keyUp: (NSEvent *) e
2243 if (! [self convertEvent:e type:KeyRelease])
2247 - (void) flagsChanged: (NSEvent *) e
2249 if (! [self convertEvent:e type:KeyPress])
2250 [super flagsChanged:e];
2254 - (NSOpenGLPixelFormat *) getGLPixelFormat
2256 NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
2258 NSOpenGLPixelFormatAttribute attrs[40];
2260 attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
2262 /* OpenGL's core profile removes a lot of the same stuff that was removed in
2263 OpenGL ES (e.g. glBegin, glDrawPixels), so it might be a possibility.
2265 opengl_core_p = True;
2266 if (opengl_core_p) {
2267 attrs[i++] = NSOpenGLPFAOpenGLProfile;
2268 attrs[i++] = NSOpenGLProfileVersion3_2Core;
2272 /* Eventually: multisampled pixmaps. May not be supported everywhere.
2273 if (multi_sample_p) {
2274 attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1;
2275 attrs[i++] = NSOpenGLPFASamples; attrs[i++] = 6;
2279 # ifdef JWXYZ_QUARTZ
2280 // Under Quartz, we're just blitting a texture.
2281 if (double_buffered_p)
2282 attrs[i++] = NSOpenGLPFADoubleBuffer;
2286 /* Under OpenGL, all sorts of drawing commands are being issued, and it might
2287 be a performance problem if this activity occurs on the front buffer.
2288 Also, some screenhacks expect OS X/iOS to always double-buffer.
2289 NSOpenGLPFABackingStore prevents flickering with screenhacks that
2290 don't redraw the entire screen every frame.
2292 attrs[i++] = NSOpenGLPFADoubleBuffer;
2293 attrs[i++] = NSOpenGLPFABackingStore;
2296 attrs[i++] = NSOpenGLPFAWindow;
2298 attrs[i++] = NSOpenGLPFAPixelBuffer;
2299 /* ...But not NSOpenGLPFAFullScreen, because that would be for
2300 [NSOpenGLContext setFullScreen].
2304 /* NSOpenGLPFAFullScreen would go here if initWithFrame's isPreview == NO.
2309 NSOpenGLPixelFormat *p = [[NSOpenGLPixelFormat alloc]
2310 initWithAttributes:attrs];
2318 - (void) stopAndClose
2320 [self stopAndClose:NO];
2324 - (void) stopAndClose:(Bool)relaunch_p
2326 if ([self isAnimating])
2327 [self stopAnimation];
2329 /* Need to make the SaverListController be the firstResponder again
2330 so that it can continue to receive its own shake events. I
2331 suppose that this abstraction-breakage means that I'm adding
2332 XScreenSaverView to the UINavigationController wrong...
2334 // UIViewController *v = [[self window] rootViewController];
2335 // if ([v isKindOfClass: [UINavigationController class]]) {
2336 // UINavigationController *n = (UINavigationController *) v;
2337 // [[n topViewController] becomeFirstResponder];
2339 [self resignFirstResponder];
2341 if (relaunch_p) { // Fake a shake on the SaverListController.
2342 [_delegate didShake:self];
2343 } else { // Not launching another, animate our return to the list.
2344 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
2345 NSLog (@"fading back to saver list");
2347 [_delegate wantsFadeOut:self];
2352 /* We distinguish between taps and drags.
2354 - Drags/pans (down, motion, up) are sent to the saver to handle.
2355 - Single-taps are sent to the saver to handle.
2356 - Double-taps are sent to the saver as a "Space" keypress.
2357 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
2358 - All taps expose the momentary "Close" button.
2361 - (void)initGestures
2363 UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
2365 action:@selector(handleDoubleTap)];
2366 dtap.numberOfTapsRequired = 2;
2367 dtap.numberOfTouchesRequired = 1;
2369 UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
2371 action:@selector(handleTap:)];
2372 stap.numberOfTapsRequired = 1;
2373 stap.numberOfTouchesRequired = 1;
2375 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
2377 action:@selector(handlePan:)];
2378 pan.maximumNumberOfTouches = 1;
2379 pan.minimumNumberOfTouches = 1;
2381 // I couldn't get Swipe to work, but using a second Pan recognizer works.
2382 UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
2384 action:@selector(handlePan2:)];
2385 pan2.maximumNumberOfTouches = 2;
2386 pan2.minimumNumberOfTouches = 2;
2388 // Also handle long-touch, and treat that the same as Pan.
2389 // Without this, panning doesn't start until there's motion, so the trick
2390 // of holding down your finger to freeze the scene doesn't work.
2392 UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
2394 action:@selector(handleLongPress:)];
2395 hold.numberOfTapsRequired = 0;
2396 hold.numberOfTouchesRequired = 1;
2397 hold.minimumPressDuration = 0.25; /* 1/4th second */
2399 // Two finger pinch to zoom in on the view.
2400 UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]
2402 action:@selector(handlePinch:)];
2404 [stap requireGestureRecognizerToFail: dtap];
2405 [stap requireGestureRecognizerToFail: hold];
2406 [dtap requireGestureRecognizerToFail: hold];
2407 [pan requireGestureRecognizerToFail: hold];
2408 [pan2 requireGestureRecognizerToFail: pinch];
2410 [self setMultipleTouchEnabled:YES];
2412 [self addGestureRecognizer: dtap];
2413 [self addGestureRecognizer: stap];
2414 [self addGestureRecognizer: pan];
2415 [self addGestureRecognizer: pan2];
2416 [self addGestureRecognizer: hold];
2417 [self addGestureRecognizer: pinch];
2428 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
2429 convert it to what X11 and OpenGL expect.
2431 Getting this crap right is tricky, given the confusion of the various
2432 scale factors, so here's a checklist that I think covers all of the X11
2433 and OpenGL cases. For each of these: rotate to all 4 orientations;
2434 ensure the mouse tracks properly to all 4 corners.
2436 Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
2438 Test hacks must cover:
2439 X11 ignoreRotation = true
2440 X11 ignoreRotation = false
2441 OpenGL (rotation is handled manually, so they never ignoreRotation)
2443 Test devices must cover:
2444 contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
2445 contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
2446 contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
2448 iPad 2: 768x1024 / 1 = 768x1024
2449 iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
2450 iPhone 4s: 640x960 / 2 = 320x480
2451 iPhone 5: 640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
2452 iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
2455 iPad2 iPadAir iPhone4s iPhone5 iPhone6+
2456 Attraction X yes - - - - Y
2457 Fireworkx X no - - - - Y
2458 Carousel GL yes - - - - Y
2459 Voronoi GL no - - - - -
2461 - (void) convertMouse:(CGPoint *)p
2463 CGFloat xx = p->x, yy = p->y;
2465 # if 0 // TARGET_IPHONE_SIMULATOR
2467 XWindowAttributes xgwa;
2468 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2469 NSLog (@"TOUCH %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2471 xgwa.width, xgwa.height,
2472 [self contentScaleFactor],
2473 [self hackedContentScaleFactor],
2474 [self rotateTouches], [self ignoreRotation]);
2476 # endif // TARGET_IPHONE_SIMULATOR
2478 if ([self rotateTouches]) {
2480 // The XScreenSaverGLView case:
2481 // The X11 window is rotated, as is the framebuffer.
2482 // The device coordinates match the framebuffer dimensions,
2483 // but might have axes swapped... and we need to swap them
2486 int w = [self frame].size.width;
2487 int h = [self frame].size.height;
2488 GLfloat xr = (GLfloat) xx / w;
2489 GLfloat yr = (GLfloat) yy / h;
2491 int o = (int) current_device_rotation();
2493 case -90: case 270: swap = xr; xr = 1-yr; yr = swap; break;
2494 case 90: case -270: swap = xr; xr = yr; yr = 1-swap; break;
2495 case 180: case -180: xr = 1-xr; yr = 1-yr; break;
2501 } else if ([self ignoreRotation]) {
2503 // The X11 case, where the hack has opted not to rotate:
2504 // The X11 window is unrotated, but the framebuffer is rotated.
2505 // The device coordinates match the framebuffer, so they need to
2506 // be de-rotated to match the X11 window.
2508 int w = [self frame].size.width;
2509 int h = [self frame].size.height;
2511 int o = (int) current_device_rotation();
2513 case -90: case 270: swap = xx; xx = h-yy; yy = swap; break;
2514 case 90: case -270: swap = xx; xx = yy; yy = w-swap; break;
2515 case 180: case -180: xx = w-xx; yy = h-yy; break;
2520 double s = [self hackedContentScaleFactor];
2524 # if 0 // TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
2526 XWindowAttributes xgwa;
2527 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2528 NSLog (@"touch %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2530 xgwa.width, xgwa.height,
2531 [self contentScaleFactor],
2532 [self hackedContentScaleFactor],
2533 [self rotateTouches], [self ignoreRotation]);
2534 if (p->x < 0 || p->y < 0 || p->x > xgwa.width || p->y > xgwa.height)
2537 # endif // TARGET_IPHONE_SIMULATOR
2541 /* Single click exits saver.
2543 - (void) handleTap:(UIGestureRecognizer *)sender
2549 memset (&xe, 0, sizeof(xe));
2551 [self showCloseButton];
2553 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2554 [self convertMouse:&p];
2555 NSAssert (xwindow->type == WINDOW, @"not a window");
2556 xwindow->window.last_mouse_x = p.x;
2557 xwindow->window.last_mouse_y = p.y;
2559 xe.xany.type = ButtonPress;
2560 xe.xbutton.button = 1;
2564 if (! [self sendEvent: &xe])
2567 xe.xany.type = ButtonRelease;
2568 xe.xbutton.button = 1;
2572 [self sendEvent: &xe];
2576 /* Double click sends Space KeyPress.
2578 - (void) handleDoubleTap
2580 if (!xsft->event_cb || !xwindow) return;
2582 [self showCloseButton];
2585 memset (&xe, 0, sizeof(xe));
2586 xe.xkey.keycode = ' ';
2587 xe.xany.type = KeyPress;
2588 BOOL ok1 = [self sendEvent: &xe];
2589 xe.xany.type = KeyRelease;
2590 BOOL ok2 = [self sendEvent: &xe];
2596 /* Drag with one finger down: send MotionNotify.
2598 - (void) handlePan:(UIGestureRecognizer *)sender
2600 if (!xsft->event_cb || !xwindow) return;
2602 [self showCloseButton];
2605 memset (&xe, 0, sizeof(xe));
2607 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2608 [self convertMouse:&p];
2609 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
2610 xwindow->window.last_mouse_x = p.x;
2611 xwindow->window.last_mouse_y = p.y;
2613 switch (sender.state) {
2614 case UIGestureRecognizerStateBegan:
2615 xe.xany.type = ButtonPress;
2616 xe.xbutton.button = 1;
2621 case UIGestureRecognizerStateEnded:
2622 xe.xany.type = ButtonRelease;
2623 xe.xbutton.button = 1;
2628 case UIGestureRecognizerStateChanged:
2629 xe.xany.type = MotionNotify;
2638 BOOL ok = [self sendEvent: &xe];
2639 if (!ok && xe.xany.type == ButtonRelease)
2644 /* Hold one finger down: assume we're about to start dragging.
2645 Treat the same as Pan.
2647 - (void) handleLongPress:(UIGestureRecognizer *)sender
2649 [self handlePan:sender];
2654 /* Drag with 2 fingers down: send arrow keys.
2656 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2658 if (!xsft->event_cb || !xwindow) return;
2660 [self showCloseButton];
2662 if (sender.state != UIGestureRecognizerStateEnded)
2666 memset (&xe, 0, sizeof(xe));
2668 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2669 [self convertMouse:&p];
2671 if (fabs(p.x) > fabs(p.y))
2672 xe.xkey.keycode = (p.x > 0 ? XK_Right : XK_Left);
2674 xe.xkey.keycode = (p.y > 0 ? XK_Down : XK_Up);
2676 BOOL ok1 = [self sendEvent: &xe];
2677 xe.xany.type = KeyRelease;
2678 BOOL ok2 = [self sendEvent: &xe];
2684 /* Pinch with 2 fingers: zoom in around the center of the fingers.
2686 - (void) handlePinch:(UIPinchGestureRecognizer *)sender
2688 if (!xsft->event_cb || !xwindow) return;
2690 [self showCloseButton];
2692 if (sender.state == UIGestureRecognizerStateBegan)
2693 pinch_transform = self.transform; // Save the base transform
2695 switch (sender.state) {
2696 case UIGestureRecognizerStateBegan:
2697 case UIGestureRecognizerStateChanged:
2699 double scale = sender.scale;
2704 self.transform = CGAffineTransformScale (pinch_transform, scale, scale);
2706 CGPoint p = [sender locationInView: self];
2707 p.x /= self.layer.bounds.size.width;
2708 p.y /= self.layer.bounds.size.height;
2710 CGPoint np = CGPointMake (self.bounds.size.width * p.x,
2711 self.bounds.size.height * p.y);
2712 CGPoint op = CGPointMake (self.bounds.size.width *
2713 self.layer.anchorPoint.x,
2714 self.bounds.size.height *
2715 self.layer.anchorPoint.y);
2716 np = CGPointApplyAffineTransform (np, self.transform);
2717 op = CGPointApplyAffineTransform (op, self.transform);
2719 CGPoint pos = self.layer.position;
2724 self.layer.position = pos;
2725 self.layer.anchorPoint = p;
2729 case UIGestureRecognizerStateEnded:
2731 // When released, snap back to the default zoom (but animate it).
2733 CABasicAnimation *a1 = [CABasicAnimation
2734 animationWithKeyPath:@"position.x"];
2735 a1.fromValue = [NSNumber numberWithFloat: self.layer.position.x];
2736 a1.toValue = [NSNumber numberWithFloat: self.bounds.size.width / 2];
2738 CABasicAnimation *a2 = [CABasicAnimation
2739 animationWithKeyPath:@"position.y"];
2740 a2.fromValue = [NSNumber numberWithFloat: self.layer.position.y];
2741 a2.toValue = [NSNumber numberWithFloat: self.bounds.size.height / 2];
2743 CABasicAnimation *a3 = [CABasicAnimation
2744 animationWithKeyPath:@"anchorPoint.x"];
2745 a3.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.x];
2746 a3.toValue = [NSNumber numberWithFloat: 0.5];
2748 CABasicAnimation *a4 = [CABasicAnimation
2749 animationWithKeyPath:@"anchorPoint.y"];
2750 a4.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.y];
2751 a4.toValue = [NSNumber numberWithFloat: 0.5];
2753 CABasicAnimation *a5 = [CABasicAnimation
2754 animationWithKeyPath:@"transform.scale"];
2755 a5.fromValue = [NSNumber numberWithFloat: sender.scale];
2756 a5.toValue = [NSNumber numberWithFloat: 1.0];
2758 CAAnimationGroup *group = [CAAnimationGroup animation];
2759 group.duration = 0.3;
2760 group.repeatCount = 1;
2761 group.autoreverses = NO;
2762 group.animations = @[ a1, a2, a3, a4, a5 ];
2763 group.timingFunction = [CAMediaTimingFunction
2765 kCAMediaTimingFunctionEaseIn];
2766 [self.layer addAnimation:group forKey:@"unpinch"];
2768 self.transform = pinch_transform;
2769 self.layer.anchorPoint = CGPointMake (0.5, 0.5);
2770 self.layer.position = CGPointMake (self.bounds.size.width / 2,
2771 self.bounds.size.height / 2);
2780 /* We need this to respond to "shake" gestures
2782 - (BOOL)canBecomeFirstResponder
2787 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2792 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2796 /* Shake means exit and launch a new saver.
2798 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2800 [self stopAndClose:YES];
2804 - (void) showCloseButton
2811 int width = self.bounds.size.width;
2812 closeBox = [[UIView alloc]
2813 initWithFrame:CGRectMake(0, 0, width, ih + off)];
2814 closeBox.backgroundColor = [UIColor clearColor];
2815 closeBox.autoresizingMask =
2816 UIViewAutoresizingFlexibleBottomMargin |
2817 UIViewAutoresizingFlexibleWidth;
2819 // Add the buttons to the bar
2820 UIImage *img1 = [UIImage imageNamed:@"stop"];
2821 UIImage *img2 = [UIImage imageNamed:@"settings"];
2823 UIButton *button = [[UIButton alloc] init];
2824 [button setFrame: CGRectMake(off, off, iw, ih)];
2825 [button setBackgroundImage:img1 forState:UIControlStateNormal];
2826 [button addTarget:self
2827 action:@selector(stopAndClose)
2828 forControlEvents:UIControlEventTouchUpInside];
2829 [closeBox addSubview:button];
2832 button = [[UIButton alloc] init];
2833 [button setFrame: CGRectMake(width - iw - off, off, iw, ih)];
2834 [button setBackgroundImage:img2 forState:UIControlStateNormal];
2835 [button addTarget:self
2836 action:@selector(stopAndOpenSettings)
2837 forControlEvents:UIControlEventTouchUpInside];
2838 button.autoresizingMask =
2839 UIViewAutoresizingFlexibleBottomMargin |
2840 UIViewAutoresizingFlexibleLeftMargin;
2841 [closeBox addSubview:button];
2844 [self addSubview:closeBox];
2847 // Don't hide the buttons under the iPhone X bezel.
2848 UIEdgeInsets is = { 0, };
2849 if ([self respondsToSelector:@selector(safeAreaInsets)]) {
2850 # pragma clang diagnostic push // "only available on iOS 11.0 or newer"
2851 # pragma clang diagnostic ignored "-Wunguarded-availability-new"
2852 is = [self safeAreaInsets];
2853 # pragma clang diagnostic pop
2854 [closeBox setFrame:CGRectMake(is.left, is.top,
2855 self.bounds.size.width - is.right - is.left,
2859 if (closeBox.layer.opacity <= 0) { // Fade in
2861 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
2862 anim.duration = 0.2;
2863 anim.repeatCount = 1;
2864 anim.autoreverses = NO;
2865 anim.fromValue = [NSNumber numberWithFloat:0.0];
2866 anim.toValue = [NSNumber numberWithFloat:1.0];
2867 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
2868 closeBox.layer.opacity = 1;
2871 // Fade out N seconds from now.
2873 [closeBoxTimer invalidate];
2874 closeBoxTimer = [NSTimer scheduledTimerWithTimeInterval: 3
2876 selector:@selector(closeBoxOff)
2884 if (closeBoxTimer) {
2885 [closeBoxTimer invalidate];
2891 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
2892 anim.duration = 0.2;
2893 anim.repeatCount = 1;
2894 anim.autoreverses = NO;
2895 anim.fromValue = [NSNumber numberWithFloat: 1];
2896 anim.toValue = [NSNumber numberWithFloat: 0];
2897 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
2898 closeBox.layer.opacity = 0;
2902 - (void) stopAndOpenSettings
2904 NSString *s = [NSString stringWithCString:xsft->progclass
2905 encoding:NSISOLatin1StringEncoding];
2906 if ([self isAnimating])
2907 [self stopAnimation];
2908 [self resignFirstResponder];
2909 [_delegate wantsFadeOut:self];
2910 [_delegate openPreferences: s];
2915 - (void)setScreenLocked:(BOOL)locked
2917 if (screenLocked == locked) return;
2918 screenLocked = locked;
2920 if ([self isAnimating])
2921 [self stopAnimation];
2923 if (! [self isAnimating])
2924 [self startAnimation];
2928 - (NSDictionary *)getGLProperties
2930 return [NSDictionary dictionaryWithObjectsAndKeys:
2931 kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
2933 /* This could be disabled if we knew the screen would be redrawn
2934 entirely for every frame.
2936 [NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
2941 - (void)addExtraRenderbuffers:(CGSize)size
2943 // No extra renderbuffers are needed for 2D screenhacks.
2947 - (NSString *)getCAGravity
2949 return kCAGravityCenter; // Looks better in e.g. Compass.
2950 // return kCAGravityBottomLeft;
2953 #endif // USE_IPHONE
2956 - (void) checkForUpdates
2959 // We only check once at startup, even if there are multiple screens,
2960 // and even if this saver is running for many days.
2961 // (Uh, except this doesn't work because this static isn't shared,
2962 // even if we make it an exported global. Not sure why. Oh well.)
2963 static BOOL checked_p = NO;
2964 if (checked_p) return;
2967 // If it's off, don't bother running the updater. Otherwise, the
2968 // updater will decide if it's time to hit the network.
2969 if (! get_boolean_resource (xdpy,
2970 SUSUEnableAutomaticChecksKey,
2971 SUSUEnableAutomaticChecksKey))
2974 NSString *updater = @"XScreenSaverUpdater.app";
2976 // There may be multiple copies of the updater: e.g., one in /Applications
2977 // and one in the mounted installer DMG! It's important that we run the
2978 // one from the disk and not the DMG, so search for the right one.
2980 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2981 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2983 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2984 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2985 @"/Library/Screen Savers",
2986 @"/System/Library/Screen Savers",
2988 @"/Applications/Utilities"];
2989 NSString *app_path = nil;
2990 for (NSString *dir in search) {
2991 NSString *p = [dir stringByAppendingPathComponent:updater];
2992 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2999 app_path = [workspace fullPathForApplication:updater];
3001 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
3002 app_path = 0; // The DMG version will not do.
3005 NSLog(@"Unable to find %@", updater);
3010 if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
3011 options:(NSWorkspaceLaunchWithoutAddingToRecents |
3012 NSWorkspaceLaunchWithoutActivation |
3013 NSWorkspaceLaunchAndHide)
3014 configuration:[NSMutableDictionary dictionary]
3016 NSLog(@"Unable to launch %@: %@", app_path, err);
3019 # endif // !USE_IPHONE
3025 /* Utility functions...
3028 static PrefsReader *
3029 get_prefsReader (Display *dpy)
3031 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
3032 if (!view) return 0;
3033 return [view prefsReader];
3038 get_string_resource (Display *dpy, char *name, char *class)
3040 return [get_prefsReader(dpy) getStringResource:name];
3044 get_boolean_resource (Display *dpy, char *name, char *class)
3046 return [get_prefsReader(dpy) getBooleanResource:name];
3050 get_integer_resource (Display *dpy, char *name, char *class)
3052 return [get_prefsReader(dpy) getIntegerResource:name];
3056 get_float_resource (Display *dpy, char *name, char *class)
3058 return [get_prefsReader(dpy) getFloatResource:name];