-/* xscreensaver, Copyright (c) 2006-2016 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2017 Jamie Zawinski <jwz@jwz.org>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
#import "XScreenSaverConfigSheet.h"
#import "Updater.h"
#import "screenhackI.h"
-#import "xlockmoreI.h"
+#import "pow2.h"
#import "jwxyzI.h"
#import "jwxyz-cocoa.h"
#import "jwxyz-timers.h"
@interface XScreenSaverView (Private)
+- (void) stopAndClose;
- (void) stopAndClose:(Bool)relaunch;
@end
# ifdef USE_IPHONE
[UIApplication sharedApplication].idleTimerDisabled =
([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
- [[UIApplication sharedApplication]
- setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
# endif
xwindow = (Window) calloc (1, sizeof(*xwindow));
// from initWithFrame.
[ogl_ctx setView:self];
+ // Get device pixels instead of points.
+ self.wantsBestResolutionOpenGLSurface = YES;
+
// This may not be necessary if there's FBO support.
# ifdef JWXYZ_GL
xwindow->window.pixfmt = pixfmt;
new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
+ // Scale factor for desktop retina displays
+ double s = [self hackedContentScaleFactor];
+ new_backbuffer_size.width *= s;
+ new_backbuffer_size.height *= s;
+
# else // USE_IPHONE
if (!ogl_ctx) {
CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
[self setViewport];
[self createBackbuffer:new_backbuffer_size];
+
+# ifdef USE_TOUCHBAR
+ static BOOL created_touchbar = NO;
+
+ if (!touchbar_view &&
+ //#### !self.isPreview &&
+ self.window.screen == [[NSScreen screens] objectAtIndex: 0] &&
+ !created_touchbar) {
+
+ // Figure out which NSScreen has the touchbar on it;
+ // find its bounds; create a saver there.
+
+ created_touchbar = YES;
+ NSScreen *tbs = [[NSScreen screens] lastObject]; // #### write me
+ NSRect rect = [tbs visibleFrame];
+
+ // #### debugging
+ rect.origin.x += 40;
+ rect.origin.x += 40;
+ rect.size.width /= 4;
+ rect.size.height /= 4;
+ NSLog(@"## TB %.0f, %.0f %.0f x %.0f",
+ rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
+
+ touchbar_view = [[[self class] alloc]
+ initWithFrame:rect
+ saverName:[NSString stringWithCString:xsft->progclass
+ encoding:NSISOLatin1StringEncoding]
+ isPreview:self.isPreview];
+ [touchbar_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
+
+ touchbar_window = [[NSWindow alloc]
+ initWithContentRect:rect
+ styleMask: (NSTitledWindowMask|NSResizableWindowMask)
+ backing:NSBackingStoreBuffered
+ defer:YES
+ screen:tbs];
+ [touchbar_window setTitle: @"XScreenSaver Touchbar"];
+ [[touchbar_window contentView] addSubview: touchbar_view];
+ [touchbar_window makeKeyAndOrderFront:touchbar_window];
+ }
+
+ if (touchbar_view) [touchbar_view startAnimation];
+# endif // USE_TOUCHBAR
}
- (void)stopAnimation
[self lockFocus]; // in case something tries to draw from here
[self prepareContext];
- /* I considered just not even calling the free callback at all...
- But webcollage-cocoa needs it, to kill the inferior webcollage
+ /* All of the xlockmore hacks need to have their release functions
+ called, or launching the same saver twice does not work. Also
+ webcollage-cocoa needs it in order to kill the inferior webcollage
processes (since the screen saver framework never generates a
- SIGPIPE for them...) Instead, I turned off the free call in
- xlockmore.c, which is where all of the bogus calls are anyway.
+ SIGPIPE for them).
*/
- xsft->free_cb (xdpy, xwindow, xdata);
+ if (xdata)
+ xsft->free_cb (xdpy, xwindow, xdata);
[self unlockFocus];
- jwxyz_free_display (xdpy);
+ jwxyz_quartz_free_display (xdpy);
xdpy = NULL;
# if defined JWXYZ_GL && !defined USE_IPHONE
CFRelease (xwindow->ogl_ctx);
//
# ifdef USE_IPHONE
[UIApplication sharedApplication].idleTimerDisabled = NO;
- [[UIApplication sharedApplication]
- setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
# endif
// Without this, the GL frame stays on screen when switching tabs
backbuffer_data = NULL;
backbuffer_len = 0;
# endif
+
+# ifdef USE_TOUCHBAR
+ if (touchbar_view) {
+ [touchbar_view stopAnimation];
+ [touchbar_window close];
+
+ [touchbar_view release];
+ [touchbar_window release];
+
+ touchbar_view = nil;
+ touchbar_window = nil;
+ }
+# endif
}
}
-#ifdef USE_IPHONE
+/* Some of the older X11 savers look bad if a "pixel" is not a thing you can
+ see. They expect big, chunky, luxurious 1990s pixels, and if they use
+ "device" pixels on a Retina screen, everything just disappears.
-/* On iPhones with Retina displays, we can draw the savers in "real"
- pixels, and that works great. The 320x480 "point" screen is really
- a 640x960 *pixel* screen. However, Retina iPads have 768x1024
- point screens which are 1536x2048 pixels, and apparently that's
- enough pixels that copying those bits to the screen is slow. Like,
- drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
- real pixels. This will probably make the savers look better
- anyway, since that's a higher resolution than most desktop monitors
- have even today. (This is only true for X11 programs, not GL
- programs. Those are fine at full rez.)
+ Retina iPads have 768x1024 point screens which are 1536x2048 pixels,
+ 2017 iMac screens are 5120x2880 in device pixels.
This method is overridden in XScreenSaverGLView, since this kludge
isn't necessary for GL programs, being resolution independent by
*/
- (CGFloat) hackedContentScaleFactor
{
- NSSize bsize = [self bounds].size;
-
- CGFloat
- max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height;
-
- // Ratio of screen size in pixels to view size in points.
+# ifdef USE_IPHONE
CGFloat s = self.contentScaleFactor;
+# else
+ CGFloat s = self.window.backingScaleFactor;
+# endif
- // Two constraints:
-
- // 1. Don't exceed -- let's say 1280 pixels in either direction.
- // (Otherwise the frame rate gets bad.)
- // Actually let's make that 1440 since iPhone 6 is natively 1334.
- CGFloat mag0 = ceil(max_bsize * s / 1440);
-
- // 2. Don't let the pixel size get too small.
- // (Otherwise pixels in IFS and similar are too fine.)
- // So don't let the result be > 2 pixels per point.
- CGFloat mag1 = ceil(s / 2);
+ if (_lowrez_p) {
+ NSSize b = [self bounds].size;
+ CGFloat wh = b.width > b.height ? b.width : b.height;
+ const int max = 800; // maybe 1024?
+ wh *= s;
+ if (wh > max)
+ s *= max / wh;
+ }
- // As of iPhone 6, mag0 is always >= mag1. This may not be true in the future.
- // (desired scale factor) = s / (desired magnification factor)
- return s / (mag0 > mag1 ? mag0 : mag1);
+ return s;
}
+#ifdef USE_IPHONE
+
double
current_device_rotation (void)
{
the opposite direction."
*/
/* statusBarOrientation deprecated in iOS 9 */
- o = [UIApplication sharedApplication].statusBarOrientation;
+ o = (UIDeviceOrientation) // from UIInterfaceOrientation
+ [UIApplication sharedApplication].statusBarOrientation;
}
switch (o) {
}
-- (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
-{
- if (i == 0) exit (-1); // Cancel
- [self stopAndClose:NO]; // Keep going
-}
-
- (void) handleException: (NSException *)e
{
NSLog (@"Caught exception: %@", e);
- [[[UIAlertView alloc] initWithTitle:
- [NSString stringWithFormat: @"%s crashed!",
- xsft->progclass]
- message:
- [NSString stringWithFormat:
- @"The error message was:"
- "\n\n%@\n\n"
- "If it keeps crashing, try "
- "resetting its options.",
- e]
- delegate: self
- cancelButtonTitle: @"Exit"
- otherButtonTitles: @"Keep going", nil]
- show];
+ UIAlertController *c = [UIAlertController
+ alertControllerWithTitle:
+ [NSString stringWithFormat: @"%s crashed!",
+ xsft->progclass]
+ message: [NSString stringWithFormat:
+ @"The error message was:"
+ "\n\n%@\n\n"
+ "If it keeps crashing, try "
+ "resetting its options.",
+ e]
+ preferredStyle:UIAlertControllerStyleAlert];
+
+ [c addAction: [UIAlertAction actionWithTitle: @"Exit"
+ style: UIAlertActionStyleDefault
+ handler: ^(UIAlertAction *a) {
+ exit (-1);
+ }]];
+ [c addAction: [UIAlertAction actionWithTitle: @"Keep going"
+ style: UIAlertActionStyleDefault
+ handler: ^(UIAlertAction *a) {
+ [self stopAndClose:NO];
+ }]];
+
+ UIViewController *vc =
+ [UIApplication sharedApplication].keyWindow.rootViewController;
+ while (vc.presentedViewController)
+ vc = vc.presentedViewController;
+ [vc presentViewController:c animated:YES completion:nil];
[self stopAnimation];
}
}
-static GLsizei
-to_pow2 (size_t x)
-{
- if (x <= 1)
- return 1;
-
- size_t mask = (size_t)-1;
- unsigned bits = sizeof(x) * CHAR_BIT;
- unsigned log2 = bits;
-
- --x;
- while (bits) {
- if (!(x & mask)) {
- log2 -= bits;
- x <<= bits;
- }
-
- bits >>= 1;
- mask <<= bits;
- }
-
- return 1 << log2;
-}
-
-
#ifdef USE_IPHONE
- (BOOL) suppressRotationAnimation
{
# ifdef USE_IPHONE
GLfloat s = self.contentScaleFactor;
- GLfloat hs = self.hackedContentScaleFactor;
# else // !USE_IPHONE
- const GLfloat s = 1;
- const GLfloat hs = s;
+ const GLfloat s = self.window.backingScaleFactor;
# endif
+ GLfloat hs = self.hackedContentScaleFactor;
// On OS X this almost isn't necessary, except for the ugly aliasing
// artifacts.
CGContextRef ob = backbuffer;
void *odata = backbuffer_data;
- size_t olen = backbuffer_len;
+ GLsizei olen = backbuffer_len;
# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
NSLog(@"backbuffer %.0fx%.0f",
if (!gl_limited_npot_p)
# endif
{
- gl_texture_w = to_pow2 (gl_texture_w);
- gl_texture_h = to_pow2 (gl_texture_h);
+ gl_texture_w = (GLsizei) to_pow2 (gl_texture_w);
+ gl_texture_h = (GLsizei) to_pow2 (gl_texture_h);
}
- size_t bytes_per_row = gl_texture_w * 4;
+ GLsizei bytes_per_row = gl_texture_w * 4;
# if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
// APPLE_client_storage requires texture width to be aligned to 32 bytes, or
new_size.width = new_size.height;
new_size.height = swap;
}
+# endif // USE_IPHONE
double s = self.hackedContentScaleFactor;
new_size.width *= s;
new_size.height *= s;
-# endif // USE_IPHONE
[self prepareContext];
[self setViewport];
if (!initted_p) {
+ resized_p = NO;
+
if (! xdpy) {
# ifdef JWXYZ_QUARTZ
xwindow->cgc = backbuffer;
# endif // JWXYZ_QUARTZ
- xdpy = jwxyz_make_display (xwindow);
+ xdpy = jwxyz_quartz_make_display (xwindow);
# if defined USE_IPHONE
/* Some X11 hacks (fluidballs) want to ignore all rotation events. */
# endif // !JWXYZ_GL
# endif // USE_IPHONE
+ _lowrez_p = get_boolean_resource (xdpy, "lowrez", "Lowrez");
+ if (_lowrez_p) {
+ resized_p = YES;
+
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+ NSSize b = [self bounds].size;
+ CGFloat s = self.hackedContentScaleFactor;
+# ifdef USE_IPHONE
+ CGFloat o = self.contentScaleFactor;
+# else
+ CGFloat o = self.window.backingScaleFactor;
+# endif
+ NSLog(@"lowrez: scaling %.0fx%.0f -> %.0fx%.0f (%.02f)",
+ b.width * o, b.height * o,
+ b.width * s, b.height * s, s);
+# endif
+ }
+
[self resize_x11];
}
xsft->setup_cb (xsft, xsft->setup_arg);
}
initted_p = YES;
- resized_p = NO;
NSAssert(!xdata, @"xdata already initialized");
if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
fpst = fps_init (xdpy, xwindow);
- if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
+ fps_cb = xsft->fps_cb;
+ if (! fps_cb) fps_cb = screenhack_do_fps;
} else {
fpst = NULL;
- xsft->fps_cb = 0;
+ fps_cb = 0;
}
# ifdef USE_IPHONE
// NSAssert(xdata, @"no xdata when drawing");
if (! xdata) abort();
unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
- if (fpst && xsft->fps_cb)
- xsft->fps_cb (xdpy, xwindow, fpst, xdata);
+ if (fpst && fps_cb)
+ fps_cb (xdpy, xwindow, fpst, xdata);
gettimeofday (&tv, 0);
now = tv.tv_sec + (tv.tv_usec / 1000000.0);
# if defined USE_IPHONE && defined JWXYZ_QUARTZ
UIGraphicsPopContext();
# endif
+
+# ifdef USE_TOUCHBAR
+ if (touchbar_view) [touchbar_view animateOneFrame];
+# endif
}
NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
toView:self];
-# ifdef USE_IPHONE
double s = [self hackedContentScaleFactor];
-# else
- int s = 1;
-# endif
int x = s * p.x;
int y = s * ([self bounds].size.height - p.y);
[e deltaX] < 0 ? Button7 :
0);
else
- xe.xbutton.button = [e buttonNumber] + 1;
+ xe.xbutton.button = (unsigned int) [e buttonNumber] + 1;
break;
case MotionNotify:
xe.xmotion.x = x;
case NSF12FunctionKey: k = XK_F12; break;
default:
{
- const char *s =
+ const char *ss =
[ns cStringUsingEncoding:NSISOLatin1StringEncoding];
- k = (s && *s ? *s : 0);
+ k = (ss && *ss ? *ss : 0);
}
break;
}
#else // USE_IPHONE
+- (void) stopAndClose
+{
+ [self stopAndClose:NO];
+}
+
+
- (void) stopAndClose:(Bool)relaunch_p
{
if ([self isAnimating])
/* We distinguish between taps and drags.
- Drags/pans (down, motion, up) are sent to the saver to handle.
- - Single-taps exit the saver.
+ - Single-taps are sent to the saver to handle.
- Double-taps are sent to the saver as a "Space" keypress.
- Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
-
- This means a saver cannot respond to a single-tap. Only a few try to.
+ - All taps expose the momentary "Close" button.
*/
- (void)initGestures
UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
initWithTarget:self
- action:@selector(handleTap)];
+ action:@selector(handleTap:)];
stap.numberOfTapsRequired = 1;
stap.numberOfTouchesRequired = 1;
hold.numberOfTouchesRequired = 1;
hold.minimumPressDuration = 0.25; /* 1/4th second */
+ // Two finger pinch to zoom in on the view.
+ UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handlePinch:)];
+
[stap requireGestureRecognizerToFail: dtap];
[stap requireGestureRecognizerToFail: hold];
[dtap requireGestureRecognizerToFail: hold];
[pan requireGestureRecognizerToFail: hold];
+ [pan2 requireGestureRecognizerToFail: pinch];
[self setMultipleTouchEnabled:YES];
[self addGestureRecognizer: pan];
[self addGestureRecognizer: pan2];
[self addGestureRecognizer: hold];
+ [self addGestureRecognizer: pinch];
[dtap release];
[stap release];
[pan release];
[pan2 release];
[hold release];
+ [pinch release];
}
{
CGFloat xx = p->x, yy = p->y;
-# if TARGET_IPHONE_SIMULATOR
+# if 0 // TARGET_IPHONE_SIMULATOR
{
XWindowAttributes xgwa;
XGetWindowAttributes (xdpy, xwindow, &xgwa);
p->x = xx * s;
p->y = yy * s;
-# if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
+# if 0 // TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
{
XWindowAttributes xgwa;
XGetWindowAttributes (xdpy, xwindow, &xgwa);
/* Single click exits saver.
*/
-- (void) handleTap
+- (void) handleTap:(UIGestureRecognizer *)sender
{
- [self stopAndClose:NO];
+ if (!xwindow)
+ return;
+
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
+
+ [self showCloseButton];
+
+ CGPoint p = [sender locationInView:self]; // this is in points, not pixels
+ [self convertMouse:&p];
+ NSAssert (xwindow->type == WINDOW, @"not a window");
+ xwindow->window.last_mouse_x = p.x;
+ xwindow->window.last_mouse_y = p.y;
+
+ xe.xany.type = ButtonPress;
+ xe.xbutton.button = 1;
+ xe.xbutton.x = p.x;
+ xe.xbutton.y = p.y;
+
+ if (! [self sendEvent: &xe])
+ ; //[self beep];
+
+ xe.xany.type = ButtonRelease;
+ xe.xbutton.button = 1;
+ xe.xbutton.x = p.x;
+ xe.xbutton.y = p.y;
+
+ [self sendEvent: &xe];
}
{
if (!xsft->event_cb || !xwindow) return;
+ [self showCloseButton];
+
XEvent xe;
memset (&xe, 0, sizeof(xe));
xe.xkey.keycode = ' ';
{
if (!xsft->event_cb || !xwindow) return;
+ [self showCloseButton];
+
XEvent xe;
memset (&xe, 0, sizeof(xe));
{
if (!xsft->event_cb || !xwindow) return;
+ [self showCloseButton];
+
if (sender.state != UIGestureRecognizerStateEnded)
return;
}
+/* Pinch with 2 fingers: zoom in around the center of the fingers.
+ */
+- (void) handlePinch:(UIPinchGestureRecognizer *)sender
+{
+ if (!xsft->event_cb || !xwindow) return;
+
+ [self showCloseButton];
+
+ if (sender.state == UIGestureRecognizerStateBegan)
+ pinch_transform = self.transform; // Save the base transform
+
+ switch (sender.state) {
+ case UIGestureRecognizerStateBegan:
+ case UIGestureRecognizerStateChanged:
+ {
+ double scale = sender.scale;
+
+ if (scale < 1)
+ return;
+
+ self.transform = CGAffineTransformScale (pinch_transform, scale, scale);
+
+ CGPoint p = [sender locationInView: self];
+ p.x /= self.layer.bounds.size.width;
+ p.y /= self.layer.bounds.size.height;
+
+ CGPoint np = CGPointMake (self.bounds.size.width * p.x,
+ self.bounds.size.height * p.y);
+ CGPoint op = CGPointMake (self.bounds.size.width *
+ self.layer.anchorPoint.x,
+ self.bounds.size.height *
+ self.layer.anchorPoint.y);
+ np = CGPointApplyAffineTransform (np, self.transform);
+ op = CGPointApplyAffineTransform (op, self.transform);
+
+ CGPoint pos = self.layer.position;
+ pos.x -= op.x;
+ pos.x += np.x;
+ pos.y -= op.y;
+ pos.y += np.y;
+ self.layer.position = pos;
+ self.layer.anchorPoint = p;
+ }
+ break;
+
+ case UIGestureRecognizerStateEnded:
+ {
+ // When released, snap back to the default zoom (but animate it).
+
+ CABasicAnimation *a1 = [CABasicAnimation
+ animationWithKeyPath:@"position.x"];
+ a1.fromValue = [NSNumber numberWithFloat: self.layer.position.x];
+ a1.toValue = [NSNumber numberWithFloat: self.bounds.size.width / 2];
+
+ CABasicAnimation *a2 = [CABasicAnimation
+ animationWithKeyPath:@"position.y"];
+ a2.fromValue = [NSNumber numberWithFloat: self.layer.position.y];
+ a2.toValue = [NSNumber numberWithFloat: self.bounds.size.height / 2];
+
+ CABasicAnimation *a3 = [CABasicAnimation
+ animationWithKeyPath:@"anchorPoint.x"];
+ a3.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.x];
+ a3.toValue = [NSNumber numberWithFloat: 0.5];
+
+ CABasicAnimation *a4 = [CABasicAnimation
+ animationWithKeyPath:@"anchorPoint.y"];
+ a4.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.y];
+ a4.toValue = [NSNumber numberWithFloat: 0.5];
+
+ CABasicAnimation *a5 = [CABasicAnimation
+ animationWithKeyPath:@"transform.scale"];
+ a5.fromValue = [NSNumber numberWithFloat: sender.scale];
+ a5.toValue = [NSNumber numberWithFloat: 1.0];
+
+ CAAnimationGroup *group = [CAAnimationGroup animation];
+ group.duration = 0.3;
+ group.repeatCount = 1;
+ group.autoreverses = NO;
+ group.animations = @[ a1, a2, a3, a4, a5 ];
+ group.timingFunction = [CAMediaTimingFunction
+ functionWithName:
+ kCAMediaTimingFunctionEaseIn];
+ [self.layer addAnimation:group forKey:@"unpinch"];
+
+ self.transform = pinch_transform;
+ self.layer.anchorPoint = CGPointMake (0.5, 0.5);
+ self.layer.position = CGPointMake (self.bounds.size.width / 2,
+ self.bounds.size.height / 2);
+ }
+ break;
+ default:
+ abort();
+ }
+}
+
+
/* We need this to respond to "shake" gestures
*/
- (BOOL)canBecomeFirstResponder
}
+- (void) showCloseButton
+{
+ double iw = 24;
+ double ih = iw;
+ double off = 4;
+
+ if (!closeBox) {
+ int width = self.bounds.size.width;
+ closeBox = [[UIView alloc]
+ initWithFrame:CGRectMake(0, 0, width, ih + off)];
+ closeBox.backgroundColor = [UIColor clearColor];
+ closeBox.autoresizingMask =
+ UIViewAutoresizingFlexibleBottomMargin |
+ UIViewAutoresizingFlexibleWidth;
+
+ // Add the buttons to the bar
+ UIImage *img1 = [UIImage imageNamed:@"stop"];
+ UIImage *img2 = [UIImage imageNamed:@"settings"];
+
+ UIButton *button = [[UIButton alloc] init];
+ [button setFrame: CGRectMake(off, off, iw, ih)];
+ [button setBackgroundImage:img1 forState:UIControlStateNormal];
+ [button addTarget:self
+ action:@selector(stopAndClose)
+ forControlEvents:UIControlEventTouchUpInside];
+ [closeBox addSubview:button];
+ [button release];
+
+ button = [[UIButton alloc] init];
+ [button setFrame: CGRectMake(width - iw - off, off, iw, ih)];
+ [button setBackgroundImage:img2 forState:UIControlStateNormal];
+ [button addTarget:self
+ action:@selector(stopAndOpenSettings)
+ forControlEvents:UIControlEventTouchUpInside];
+ button.autoresizingMask =
+ UIViewAutoresizingFlexibleBottomMargin |
+ UIViewAutoresizingFlexibleLeftMargin;
+ [closeBox addSubview:button];
+ [button release];
+
+ [self addSubview:closeBox];
+ }
+
+ // Don't hide the buttons under the iPhone X bezel.
+ UIEdgeInsets is = { 0, };
+ if ([self respondsToSelector:@selector(safeAreaInsets)]) {
+# pragma clang diagnostic push // "only available on iOS 11.0 or newer"
+# pragma clang diagnostic ignored "-Wunguarded-availability-new"
+ is = [self safeAreaInsets];
+# pragma clang diagnostic pop
+ [closeBox setFrame:CGRectMake(is.left, is.top,
+ self.bounds.size.width - is.right - is.left,
+ ih + off)];
+ }
+
+ if (closeBox.layer.opacity <= 0) { // Fade in
+
+ CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ anim.duration = 0.2;
+ anim.repeatCount = 1;
+ anim.autoreverses = NO;
+ anim.fromValue = [NSNumber numberWithFloat:0.0];
+ anim.toValue = [NSNumber numberWithFloat:1.0];
+ [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
+ closeBox.layer.opacity = 1;
+ }
+
+ // Fade out N seconds from now.
+ if (closeBoxTimer)
+ [closeBoxTimer invalidate];
+ closeBoxTimer = [NSTimer scheduledTimerWithTimeInterval: 3
+ target:self
+ selector:@selector(closeBoxOff)
+ userInfo:nil
+ repeats:NO];
+}
+
+
+- (void)closeBoxOff
+{
+ if (closeBoxTimer) {
+ [closeBoxTimer invalidate];
+ closeBoxTimer = 0;
+ }
+ if (!closeBox)
+ return;
+
+ CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ anim.duration = 0.2;
+ anim.repeatCount = 1;
+ anim.autoreverses = NO;
+ anim.fromValue = [NSNumber numberWithFloat: 1];
+ anim.toValue = [NSNumber numberWithFloat: 0];
+ [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
+ closeBox.layer.opacity = 0;
+}
+
+
+- (void) stopAndOpenSettings
+{
+ NSString *s = [NSString stringWithCString:xsft->progclass
+ encoding:NSISOLatin1StringEncoding];
+ if ([self isAnimating])
+ [self stopAnimation];
+ [self resignFirstResponder];
+ [_delegate wantsFadeOut:self];
+ [_delegate openPreferences: s];
+
+}
+
+
- (void)setScreenLocked:(BOOL)locked
{
if (screenLocked == locked) return;