-/* xscreensaver, Copyright (c) 2006-2016 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2018 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"
#ifndef MAC_OS_X_VERSION_10_6
# define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
#endif
-#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */
+#ifndef MAC_OS_X_VERSION_10_12
+# define MAC_OS_X_VERSION_10_12 101200
+#endif
+#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 && \
+ MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12)
+ /* 10.6 SDK or later, and earlier than 10.12 SDK */
# import <objc/objc-auto.h>
# define DO_GC_HACKERY
#endif
@interface XScreenSaverView (Private)
+- (void) stopAndClose;
- (void) stopAndClose:(Bool)relaunch;
@end
[self setBackgroundColor:[NSColor blackColor]];
# endif // USE_IPHONE
+# ifdef JWXYZ_QUARTZ
+ // Colorspaces and CGContexts only happen with non-GL hacks.
+ colorspace = CGColorSpaceCreateDeviceRGB ();
+# endif
+
+ return self;
+}
+
+
+#ifdef USE_TOUCHBAR
+- (id) initWithFrame:(NSRect)frame
+ saverName:(NSString *)saverName
+ isPreview:(BOOL)isPreview
+ isTouchbar:(BOOL)isTouchbar
+{
+ if (! (self = [self initWithFrame:frame saverName:saverName
+ isPreview:isPreview]))
+ return 0;
+ touchbar_p = isTouchbar;
return self;
}
+#endif // USE_TOUCHBAR
#ifdef USE_IPHONE
# 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
+ 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_view release];
+ touchbar_view = nil;
+ }
+# endif
}
}
+#ifdef USE_TOUCHBAR
+
+static NSString *touchbar_cid = @"org.jwz.xscreensaver.touchbar";
+static NSString *touchbar_iid = @"org.jwz.xscreensaver.touchbar";
+
+- (NSTouchBar *) makeTouchBar
+{
+ NSTouchBar *t = [[NSTouchBar alloc] init];
+ t.delegate = self;
+ t.customizationIdentifier = touchbar_cid;
+ t.defaultItemIdentifiers = @[touchbar_iid,
+ NSTouchBarItemIdentifierOtherItemsProxy];
+ t.customizationAllowedItemIdentifiers = @[touchbar_iid];
+ t.principalItemIdentifier = touchbar_iid;
+ return t;
+}
+
+- (NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar
+ makeItemForIdentifier:(NSTouchBarItemIdentifier)id
+{
+ if ([id isEqualToString:touchbar_iid])
+ {
+ NSRect rect = [self frame];
+ // #### debugging
+ rect.origin.x = 0;
+ rect.origin.y = 0;
+ rect.size.width = 200;
+ rect.size.height = 40;
+ touchbar_view = [[[self class] alloc]
+ initWithFrame:rect
+ saverName:[NSString stringWithCString:xsft->progclass
+ encoding:NSISOLatin1StringEncoding]
+ isPreview:self.isPreview
+ isTouchbar:True];
+ [touchbar_view setAutoresizingMask:
+ NSViewWidthSizable|NSViewHeightSizable];
+ NSCustomTouchBarItem *item =
+ [[NSCustomTouchBarItem alloc] initWithIdentifier:id];
+ item.view = touchbar_view;
+ item.customizationLabel = touchbar_cid;
+
+ if ([self isAnimating])
+ // TouchBar was created after animation begun.
+ [touchbar_view startAnimation];
+ }
+ return nil;
+}
+
+#endif // USE_TOUCHBAR
+
+
static void
screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
{
}
-#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);
+ if (_lowrez_p) {
+ NSSize b = [self bounds].size;
+ CGFloat wh = b.width > b.height ? b.width : b.height;
- // 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);
+ // Scale down to as close to 1024 as we can get without going under,
+ // while keeping an integral scale factor so that we don't get banding
+ // artifacts and moire patterns.
+ //
+ // Retina sizes: 2208 => 1104, 2224 => 1112, 2732 => 1366, 2880 => 1440.
+ //
+ int s2 = wh / 1024;
+ if (s2) s /= s2;
+ }
- // 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];
}
gl_texture_target = GL_TEXTURE_2D;
# endif
- glBindTexture (gl_texture_target, &backbuffer_texture);
+ glBindTexture (gl_texture_target, backbuffer_texture);
glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// GL_LINEAR might make sense on Retina iPads.
glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
-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.
*/
- (void) createBackbuffer:(CGSize)new_size
{
- // Colorspaces and CGContexts only happen with non-GL hacks.
- if (colorspace)
- CGColorSpaceRelease (colorspace);
-
- NSWindow *window = [self window];
-
- if (window && xdpy) {
- [self lockFocus];
-
-# ifdef BACKBUFFER_OPENGL
- // Was apparently faster until 10.9.
- colorspace = CGColorSpaceCreateDeviceRGB ();
-# endif // BACKBUFFER_OPENGL
-
- [self unlockFocus];
- } else {
- colorspace = CGColorSpaceCreateDeviceRGB();
- }
-
CGSize osize = CGSizeZero;
if (backbuffer) {
osize.width = CGBitmapContextGetWidth(backbuffer);
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
// landscape shape, swap width and height to keep the backbuffer
// in portrait.
//
- if ([self ignoreRotation] && new_size.width > new_size.height) {
+ double rot = current_device_rotation();
+ if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
CGFloat swap = new_size.width;
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];
// On first resize, xwindow->frame is 0x0.
xwindow->frame.height == new_size.height)
return;
- [self prepareContext];
-
# if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
[ogl_ctx update];
# endif // BACKBUFFER_OPENGL && !USE_IPHONE
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
+ if (o != s)
+ 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);
{
// Render X11 into the backing store bitmap...
+# ifdef USE_TOUCHBAR
+ if (touchbar_p) return;
+# endif
+
# ifdef JWXYZ_QUARTZ
NSAssert (backbuffer, @"no back buffer");
# 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;