-/* xscreensaver, Copyright (c) 2006-2013 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
-* the above copyright notice appear in all copies and that both that
-* copyright notice and this permission notice appear in supporting
-* documentation. No representations are made about the suitability of this
-* software for any purpose. It is provided "as is" without express or
-* implied warranty.
-*/
+/* xscreensaver, Copyright (c) 2006-2014 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
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation. No representations are made about the suitability of this
+ * software for any purpose. It is provided "as is" without express or
+ * implied warranty.
+ */
/* This is a subclass of Apple's ScreenSaverView that knows how to run
xscreensaver programs without X11 via the dark magic of the "jwxyz"
*/
#import <QuartzCore/QuartzCore.h>
+#import <zlib.h>
#import "XScreenSaverView.h"
#import "XScreenSaverConfigSheet.h"
+#import "Updater.h"
#import "screenhackI.h"
#import "xlockmoreI.h"
#import "jwxyz-timers.h"
{ "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
{ "-fps", ".doFPS", XrmoptionNoArg, "True" },
{ "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
+ { "-foreground", ".foreground", XrmoptionSepArg, 0 },
+ { "-fg", ".foreground", XrmoptionSepArg, 0 },
+ { "-background", ".background", XrmoptionSepArg, 0 },
+ { "-bg", ".background", XrmoptionSepArg, 0 },
+
+# ifndef USE_IPHONE
+ // <xscreensaver-updater />
+ { "-" SUSUEnableAutomaticChecksKey,
+ "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" },
+ { "-no-" SUSUEnableAutomaticChecksKey,
+ "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
+ { "-" SUAutomaticallyUpdateKey,
+ "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" },
+ { "-no-" SUAutomaticallyUpdateKey,
+ "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
+ { "-" SUSendProfileInfoKey,
+ "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
+ { "-no-" SUSendProfileInfoKey,
+ "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
+ { "-" SUScheduledCheckIntervalKey,
+ "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
+# endif // !USE_IPHONE
+
{ 0, 0, 0, 0 }
};
static const char *default_defaults [] = {
# endif
".imageDirectory: ~/Pictures",
".relaunchDelay: 2",
+
+# ifndef USE_IPHONE
+# define STR1(S) #S
+# define STR(S) STR1(S)
+# define __objc_yes Yes
+# define __objc_no No
+ "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
+ "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef),
+ "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef),
+ "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef),
+# undef __objc_yes
+# undef __objc_no
+# undef STR1
+# undef STR
+# endif // USE_IPHONE
0
};
isPreview:(BOOL)isPreview
{
# ifdef USE_IPHONE
+ initial_bounds = frame.size;
rot_current_size = frame.size; // needs to be early, because
rot_from = rot_current_size; // [self setFrame] is called by
rot_to = rot_current_size; // [super initWithFrame].
[self setMultipleTouchEnabled:YES];
orientation = UIDeviceOrientationUnknown;
[self didRotate:nil];
+ [self initGestures];
# endif // USE_IPHONE
setup_p = YES;
- (void) initLayer
{
-# ifndef USE_IPHONE
+# if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER)
[self setLayer: [CALayer layer]];
+ self.layer.delegate = self;
+ self.layer.opaque = YES;
[self setWantsLayer: YES];
-# endif
+# endif // !USE_IPHONE && BACKBUFFER_CALAYER
}
{
NSAssert(![self isAnimating], @"still animating");
NSAssert(!xdata, @"xdata not yet freed");
- if (xdpy)
- jwxyz_free_display (xdpy);
+ NSAssert(!xdpy, @"xdpy not yet freed");
# ifdef USE_BACKBUFFER
if (backbuffer)
CGContextRelease (backbuffer);
-# endif
+
+ if (colorspace)
+ CGColorSpaceRelease (colorspace);
+
+# ifdef BACKBUFFER_CGCONTEXT
+ if (window_ctx)
+ CGContextRelease (window_ctx);
+# endif // BACKBUFFER_CGCONTEXT
+
+# endif // USE_BACKBUFFER
[prefsReader release];
{
NSAssert(![self isAnimating], @"already animating");
NSAssert(!initted_p && !xdata, @"already initialized");
+
+ // See comment in render_x11() for why this value is important:
+ [self setAnimationTimeInterval: 1.0 / 120.0];
+
[super startAnimation];
/* We can't draw on the window from this method, so we actually do the
initialization of the screen saver (xsft->init_cb) in the first call
# ifdef USE_IPHONE
[UIApplication sharedApplication].idleTimerDisabled =
([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
+ [[UIApplication sharedApplication]
+ setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
# endif
}
xsft->free_cb (xdpy, xwindow, xdata);
[self unlockFocus];
+ // xdpy must be freed before dealloc is called, because xdpy owns a
+ // circular reference to the parent XScreenSaverView.
+ jwxyz_free_display (xdpy);
+ xdpy = NULL;
+ xwindow = NULL;
+
// setup_p = NO; // #### wait, do we need this?
initted_p = NO;
xdata = 0;
//
# ifdef USE_IPHONE
[UIApplication sharedApplication].idleTimerDisabled = NO;
+ [[UIApplication sharedApplication]
+ setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
# endif
}
- (CGFloat) hackedContentScaleFactor
{
GLfloat s = [self contentScaleFactor];
- CGRect frame = [self bounds];
- if (frame.size.width >= 1024 ||
- frame.size.height >= 1024)
+ if (initial_bounds.width >= 1024 ||
+ initial_bounds.height >= 1024)
s = 1;
return s;
}
{
# ifdef USE_IPHONE
double s = [self hackedContentScaleFactor];
- int new_w = s * rot_current_size.width;
- int new_h = s * rot_current_size.height;
+ CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size;
+ int new_w = s * rotsize.width;
+ int new_h = s * rotsize.height;
# else
int new_w = [self bounds].size.width;
int new_h = [self bounds].size.height;
# endif
+
+ // Colorspaces and CGContexts only happen with non-GL hacks.
+ if (colorspace)
+ CGColorSpaceRelease (colorspace);
+# ifdef BACKBUFFER_CGCONTEXT
+ if (window_ctx)
+ CGContextRelease (window_ctx);
+# endif
+
+ NSWindow *window = [self window];
+
+ if (window && xdpy) {
+ [self lockFocus];
+
+# if defined(BACKBUFFER_CGCONTEXT)
+ // TODO: This was borrowed from jwxyz_window_resized, and should
+ // probably be refactored.
+
+ // Figure out which screen the window is currently on.
+ CGDirectDisplayID cgdpy = 0;
+
+ {
+// int wx, wy;
+// TODO: XTranslateCoordinates is returning (0,1200) on my system.
+// Is this right?
+// In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
+// XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
+// p.x = wx;
+// p.y = wy;
+
+ NSPoint p0 = {0, 0};
+ p0 = [window convertBaseToScreen:p0];
+ CGPoint p = {p0.x, p0.y};
+ CGDisplayCount n;
+ CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
+ NSAssert (cgdpy, @"unable to find CGDisplay");
+ }
+
+ {
+ // Figure out this screen's colorspace, and use that for every CGImage.
+ //
+ CMProfileRef profile = 0;
+
+ // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
+ // documented replacement as of OS X 10.9.
+ // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
+ CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
+ NSAssert (profile, @"unable to find colorspace profile");
+ colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
+ NSAssert (colorspace, @"unable to find colorspace");
+ }
+# elif defined(BACKBUFFER_CALAYER)
+ // Was apparently faster until 10.9.
+ colorspace = CGColorSpaceCreateDeviceRGB ();
+# endif // BACKBUFFER_CALAYER
+
+# ifdef BACKBUFFER_CGCONTEXT
+ window_ctx = [[window graphicsContext] graphicsPort];
+ CGContextRetain (window_ctx);
+# endif // BACKBUFFER_CGCONTEXT
+
+ [self unlockFocus];
+ } else {
+# ifdef BACKBUFFER_CGCONTEXT
+ window_ctx = NULL;
+# endif // BACKBUFFER_CGCONTEXT
+ colorspace = CGColorSpaceCreateDeviceRGB();
+ }
if (backbuffer &&
backbuffer_size.width == new_w &&
backbuffer_size.width = new_w;
backbuffer_size.height = new_h;
- CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
backbuffer = CGBitmapContextCreate (NULL,
backbuffer_size.width,
backbuffer_size.height,
8,
backbuffer_size.width * 4,
- cs,
- kCGImageAlphaPremultipliedLast);
- CGColorSpaceRelease (cs);
+ colorspace,
+ // kCGImageAlphaPremultipliedLast
+ (kCGImageAlphaNoneSkipFirst |
+ kCGBitmapByteOrder32Host)
+ );
NSAssert (backbuffer, @"unable to allocate back buffer");
// Clear it.
xdpy = jwxyz_make_display (self, 0);
# endif
xwindow = XRootWindow (xdpy, 0);
+
+# ifdef USE_IPHONE
+ /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
+ ignore_rotation_p =
+ get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
+# endif // USE_IPHONE
+
[self resize_x11];
}
initted_p = YES;
resized_p = NO;
NSAssert(!xdata, @"xdata already initialized");
-
-# ifdef USE_IPHONE
- /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
- ignore_rotation_p =
- get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
-# endif // USE_IPHONE
# undef ya_rand_init
(void *(*) (Display *, Window, void *)) xsft->init_cb;
xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
+ // NSAssert(xdata, @"no xdata from init");
+ if (! xdata) abort();
if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
fpst = fps_init (xdpy, xwindow);
if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
+ } else {
+ fpst = NULL;
+ xsft->fps_cb = 0;
}
+
+ [self checkForUpdates];
}
}
- /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
- This is bad, because some of the screen hacks want to delay for long
- periods (like 5 seconds or a minute!) between frames, and running them
- all at 60 FPS is no good.
-
- So, we don't use setAnimationTimeInterval, and just let the framework call
- us whenever. But, we only invoke the screen hack's "draw frame" method
- when enough time has expired.
+ /* It turns out that on some systems (possibly only 10.5 and older?)
+ [ScreenSaverView setAnimationTimeInterval] does nothing. This means
+ that we cannot rely on it.
+
+ Some of the screen hacks want to delay for long periods, and letting the
+ framework run the update function at 30 FPS when it really wanted half a
+ minute between frames would be bad. So instead, we assume that the
+ framework's animation timer might fire whenever, but we only invoke the
+ screen hack's "draw frame" method when enough time has expired.
This means two extra calls to gettimeofday() per frame. For fast-cycling
screen savers, that might actually slow them down. Oh well.
- #### Also, we do not run the draw callback faster than the system's
- animationTimeInterval, so if any savers are pickier about timing
- than that, this may slow them down too much. If that's a problem,
- then we could call draw_cb in a loop here (with usleep) until the
- next call would put us past animationTimeInterval... But a better
- approach would probably be to just change the saver to not do that.
+ A side-effect of this is that it's not possible for a saver to request
+ an animation interval that is faster than animationTimeInterval.
+
+ HOWEVER! On modern systems where setAnimationTimeInterval is *not*
+ ignored, it's important that it be faster than 30 FPS. 120 FPS is good.
+
+ An NSTimer won't fire if the timer is already running the invocation
+ function from a previous firing. So, if we use a 30 FPS
+ animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
+ frame, there will be a 26666 µs delay until the next frame, 66666 µs
+ after the beginning of the current frame. In other words, 25 FPS
+ becomes 15 FPS.
+
+ Frame rates tend to snap to values of 30/N, where N is a positive
+ integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
+ is rounded down from what it would normally be.
+
+ So if we set animationTimeInterval to 1/120 instead of 1/30, frame rates
+ become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
+ steps for higher or lower animation time intervals respectively.
*/
struct timeval tv;
gettimeofday (&tv, 0);
# ifndef USE_IPHONE
NSDisableScreenUpdates();
# endif
+ // NSAssert(xdata, @"no xdata when drawing");
+ if (! xdata) abort();
unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
# ifndef USE_IPHONE
- (void) animateOneFrame
{
[self render_x11];
+ jwxyz_flush_context(xdpy);
}
#else // USE_BACKBUFFER
# ifdef USE_IPHONE
// Then compute the transformations for rotation.
-
- if (!ignore_rotation_p) {
- // The rotation origin for layer.affineTransform is in the center already.
- CGAffineTransform t =
- CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
-
- // Correct the aspect ratio.
- CGRect frame = [self bounds];
- double s = [self hackedContentScaleFactor];
- t = CGAffineTransformScale(t,
- backbuffer_size.width / (s * frame.size.width),
- backbuffer_size.height / (s * frame.size.height));
- self.layer.affineTransform = t;
- }
+ double hs = [self hackedContentScaleFactor];
+ double s = [self contentScaleFactor];
+
+ // The rotation origin for layer.affineTransform is in the center already.
+ CGAffineTransform t = ignore_rotation_p ?
+ CGAffineTransformIdentity :
+ CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
+
+ CGFloat f = s / hs;
+ self.layer.affineTransform = CGAffineTransformScale(t, f, f);
+
+ CGRect bounds;
+ bounds.origin.x = 0;
+ bounds.origin.y = 0;
+ bounds.size.width = backbuffer_size.width / s;
+ bounds.size.height = backbuffer_size.height / s;
+ self.layer.bounds = bounds;
# endif // USE_IPHONE
+
+# if defined(BACKBUFFER_CALAYER)
+ [self.layer setNeedsDisplay];
+# elif defined(BACKBUFFER_CGCONTEXT)
+ size_t
+ w = CGBitmapContextGetWidth (backbuffer),
+ h = CGBitmapContextGetHeight (backbuffer);
+
+ size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
+ CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
+ CGBitmapContextGetData(backbuffer),
+ bpl * h,
+ NULL);
+
+
+ CGImageRef img = CGImageCreate (w, h,
+ 8, 32,
+ CGBitmapContextGetBytesPerRow(backbuffer),
+ colorspace,
+ CGBitmapContextGetBitmapInfo(backbuffer),
+ prov, NULL, NO,
+ kCGRenderingIntentDefault);
+
+ CGDataProviderRelease (prov);
+
+ CGRect rect;
+ rect.origin.x = 0;
+ rect.origin.y = 0;
+ rect.size = backbuffer_size;
+ CGContextDrawImage (window_ctx, rect, img);
+
+ CGImageRelease (img);
- // Then copy that bitmap to the screen, by just stuffing it into
- // the layer. The superclass drawRect method will handle the rest.
+ CGContextFlush (window_ctx);
+# endif // BACKBUFFER_CGCONTEXT
+}
- CGImageRef img = CGBitmapContextCreateImage (backbuffer);
- self.layer.contents = (id)img;
- CGImageRelease (img);
+# ifdef BACKBUFFER_CALAYER
+
+- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
+{
+ // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
+ char *dest_data = (char *)CGBitmapContextGetData (ctx);
+
+ // The CGContext here is normally upside-down on iOS.
+ if (dest_data &&
+ CGBitmapContextGetBitmapInfo (ctx) ==
+ (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
+# ifdef USE_IPHONE
+ && CGContextGetCTM (ctx).d < 0
+# endif // USE_IPHONE
+ )
+ {
+ size_t dest_height = CGBitmapContextGetHeight (ctx);
+ size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
+ size_t src_height = CGBitmapContextGetHeight (backbuffer);
+ size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
+ char *src_data = (char *)CGBitmapContextGetData (backbuffer);
+
+ size_t height = src_height < dest_height ? src_height : dest_height;
+
+ if (src_bpr == dest_bpr) {
+ // iPad 1: 4.0 ms, iPad 2: 6.7 ms
+ memcpy (dest_data, src_data, src_bpr * height);
+ } else {
+ // iPad 1: 4.6 ms, iPad 2: 7.2 ms
+ size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
+ while (height) {
+ memcpy (dest_data, src_data, bpr);
+ --height;
+ src_data += src_bpr;
+ dest_data += dest_bpr;
+ }
+ }
+ } else {
+
+ // iPad 1: 9.6 ms, iPad 2: 12.1 ms
+
+# ifdef USE_IPHONE
+ CGContextScaleCTM (ctx, 1, -1);
+ CGFloat s = [self contentScaleFactor];
+ CGFloat hs = [self hackedContentScaleFactor];
+ CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
+# endif // USE_IPHONE
+
+ CGImageRef img = CGBitmapContextCreateImage (backbuffer);
+ CGContextDrawImage (ctx, self.layer.bounds, img);
+ CGImageRelease (img);
+ }
}
+# endif // BACKBUFFER_CALAYER
-#endif // !USE_BACKBUFFER
+#endif // USE_BACKBUFFER
return YES;
}
++ (NSString *) decompressXML: (NSData *)data
+{
+ if (! data) return 0;
+ BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
+
+ // If it's not already XML, decompress it.
+ NSAssert (compressed_p, @"xml isn't compressed");
+ if (compressed_p) {
+ NSMutableData *data2 = 0;
+ int ret = -1;
+ z_stream zs;
+ memset (&zs, 0, sizeof(zs));
+ ret = inflateInit2 (&zs, 16 + MAX_WBITS);
+ if (ret == Z_OK) {
+ UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
+ data2 = [NSMutableData dataWithLength: usize];
+ zs.next_in = (Bytef *) data.bytes;
+ zs.avail_in = (uint) data.length;
+ zs.next_out = (Bytef *) data2.bytes;
+ zs.avail_out = (uint) data2.length;
+ ret = inflate (&zs, Z_FINISH);
+ inflateEnd (&zs);
+ }
+ if (ret == Z_OK || ret == Z_STREAM_END)
+ data = data2;
+ else
+ NSAssert2 (0, @"gunzip error: %d: %s",
+ ret, (zs.msg ? zs.msg : "<null>"));
+ }
+
+ return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+}
+
+
#ifndef USE_IPHONE
- (NSWindow *) configureSheet
#else
NSWindow *sheet;
# endif // !USE_IPHONE
+ NSData *xmld = [NSData dataWithContentsOfFile:path];
+ NSString *xml = [[self class] decompressXML: xmld];
sheet = [[XScreenSaverConfigSheet alloc]
- initWithXMLFile:path
- options:xsft->options
- controller:[prefsReader userDefaultsController]
- defaults:[prefsReader defaultOptions]];
+ initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
+ options:xsft->options
+ controller:[prefsReader userDefaultsController]
+ globalController:[prefsReader globalDefaultsController]
+ defaults:[prefsReader defaultOptions]];
// #### am I expected to retain this, or not? wtf.
// I thought not, but if I don't do this, we (sometimes) crash.
// #### Analyze says "potential leak of an object stored into sheet"
- [sheet retain];
+ // [sheet retain];
return sheet;
}
/* Announce our willingness to accept keyboard input.
-*/
+ */
- (BOOL)acceptsFirstResponder
{
return YES;
}
+- (void) beep
+{
+# ifndef USE_IPHONE
+ NSBeep();
+# else // USE_IPHONE
+
+ // There's no way to play a standard system alert sound!
+ // We'd have to include our own WAV for that.
+ //
+ // Or we could vibrate:
+ // #import <AudioToolbox/AudioToolbox.h>
+ // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
+ //
+ // Instead, just flash the screen white, then fade.
+ //
+ UIView *v = [[UIView alloc] initWithFrame: [self frame]];
+ [v setBackgroundColor: [UIColor whiteColor]];
+ [[self window] addSubview:v];
+ [UIView animateWithDuration: 0.1
+ animations:^{ [v setAlpha: 0.0]; }
+ completion:^(BOOL finished) { [v removeFromSuperview]; } ];
+
+# endif // USE_IPHONE
+}
+
+
+/* Send an XEvent to the hack. Returns YES if it was handled.
+ */
+- (BOOL) sendEvent: (XEvent *) e
+{
+ if (!initted_p || ![self isAnimating]) // no event handling unless running.
+ return NO;
+
+ [self lockFocus];
+ [self prepareContext];
+ BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
+ [self unlockFocus];
+ return result;
+}
+
+
#ifndef USE_IPHONE
/* Convert an NSEvent into an XEvent, and pass it along.
Returns YES if it was handled.
*/
-- (BOOL) doEvent: (NSEvent *) e
+- (BOOL) convertEvent: (NSEvent *) e
type: (int) type
{
- if (![self isPreview] || // no event handling if actually screen-saving!
- ![self isAnimating] ||
- !initted_p)
- return NO;
-
XEvent xe;
memset (&xe, 0, sizeof(xe));
break;
}
- [self lockFocus];
- [self prepareContext];
- BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
- [self unlockFocus];
- return result;
+ return [self sendEvent: &xe];
}
- (void) mouseDown: (NSEvent *) e
{
- if (! [self doEvent:e type:ButtonPress])
+ if (! [self convertEvent:e type:ButtonPress])
[super mouseDown:e];
}
- (void) mouseUp: (NSEvent *) e
{
- if (! [self doEvent:e type:ButtonRelease])
+ if (! [self convertEvent:e type:ButtonRelease])
[super mouseUp:e];
}
- (void) otherMouseDown: (NSEvent *) e
{
- if (! [self doEvent:e type:ButtonPress])
+ if (! [self convertEvent:e type:ButtonPress])
[super otherMouseDown:e];
}
- (void) otherMouseUp: (NSEvent *) e
{
- if (! [self doEvent:e type:ButtonRelease])
+ if (! [self convertEvent:e type:ButtonRelease])
[super otherMouseUp:e];
}
- (void) mouseMoved: (NSEvent *) e
{
- if (! [self doEvent:e type:MotionNotify])
+ if (! [self convertEvent:e type:MotionNotify])
[super mouseMoved:e];
}
- (void) mouseDragged: (NSEvent *) e
{
- if (! [self doEvent:e type:MotionNotify])
+ if (! [self convertEvent:e type:MotionNotify])
[super mouseDragged:e];
}
- (void) otherMouseDragged: (NSEvent *) e
{
- if (! [self doEvent:e type:MotionNotify])
+ if (! [self convertEvent:e type:MotionNotify])
[super otherMouseDragged:e];
}
- (void) scrollWheel: (NSEvent *) e
{
- if (! [self doEvent:e type:ButtonPress])
+ if (! [self convertEvent:e type:ButtonPress])
[super scrollWheel:e];
}
- (void) keyDown: (NSEvent *) e
{
- if (! [self doEvent:e type:KeyPress])
+ if (! [self convertEvent:e type:KeyPress])
[super keyDown:e];
}
- (void) keyUp: (NSEvent *) e
{
- if (! [self doEvent:e type:KeyRelease])
+ if (! [self convertEvent:e type:KeyRelease])
[super keyUp:e];
}
- (void) flagsChanged: (NSEvent *) e
{
- if (! [self doEvent:e type:KeyPress])
+ if (! [self convertEvent:e type:KeyPress])
[super flagsChanged:e];
}
Possibly XScreenSaverView should use Core Animation, and
XScreenSaverGLView should override that.
-*/
+ */
- (void)didRotate:(NSNotification *)notification
{
UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
default: angle_to = 0; break;
}
- NSRect ff = [self bounds];
-
switch (orientation) {
case UIDeviceOrientationLandscapeRight: // from landscape
case UIDeviceOrientationLandscapeLeft:
- rot_from.width = ff.size.height;
- rot_from.height = ff.size.width;
+ rot_from.width = initial_bounds.height;
+ rot_from.height = initial_bounds.width;
break;
default: // from portrait
- rot_from.width = ff.size.width;
- rot_from.height = ff.size.height;
+ rot_from.width = initial_bounds.width;
+ rot_from.height = initial_bounds.height;
break;
}
switch (new_orientation) {
case UIDeviceOrientationLandscapeRight: // to landscape
case UIDeviceOrientationLandscapeLeft:
- rot_to.width = ff.size.height;
- rot_to.height = ff.size.width;
+ rot_to.width = initial_bounds.height;
+ rot_to.height = initial_bounds.width;
break;
default: // to portrait
- rot_to.width = ff.size.width;
- rot_to.height = ff.size.height;
+ rot_to.width = initial_bounds.width;
+ rot_to.height = initial_bounds.height;
break;
}
}
-/* I believe we can't use UIGestureRecognizer for tracking touches
- because UIPanGestureRecognizer doesn't give us enough detail in its
- callbacks.
+/* We distinguish between taps and drags.
- Currently we don't handle multi-touches (just the first touch) but
- I'm leaving this comment here for future reference:
+ - Drags/pans (down, motion, up) are sent to the saver to handle.
+ - Single-taps exit the saver.
+ - Double-taps are sent to the saver as a "Space" keypress.
+ - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
- In the simulator, multi-touch sequences look like this:
+ This means a saver cannot respond to a single-tap. Only a few try to.
+ */
- touchesBegan [touchA, touchB]
- touchesEnd [touchA, touchB]
+- (void)initGestures
+{
+ UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleDoubleTap)];
+ dtap.numberOfTapsRequired = 2;
+ dtap.numberOfTouchesRequired = 1;
+
+ UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleTap)];
+ stap.numberOfTapsRequired = 1;
+ stap.numberOfTouchesRequired = 1;
+
+ UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handlePan:)];
+ pan.maximumNumberOfTouches = 1;
+ pan.minimumNumberOfTouches = 1;
+
+ // I couldn't get Swipe to work, but using a second Pan recognizer works.
+ UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handlePan2:)];
+ pan2.maximumNumberOfTouches = 2;
+ pan2.minimumNumberOfTouches = 2;
+
+ // Also handle long-touch, and treat that the same as Pan.
+ // Without this, panning doesn't start until there's motion, so the trick
+ // of holding down your finger to freeze the scene doesn't work.
+ //
+ UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleLongPress:)];
+ hold.numberOfTapsRequired = 0;
+ hold.numberOfTouchesRequired = 1;
+ hold.minimumPressDuration = 0.25; /* 1/4th second */
- But on real devices, sometimes you get that, but sometimes you get:
+ [stap requireGestureRecognizerToFail: dtap];
+ [stap requireGestureRecognizerToFail: hold];
+ [dtap requireGestureRecognizerToFail: hold];
+ [pan requireGestureRecognizerToFail: hold];
- touchesBegan [touchA, touchB]
- touchesEnd [touchB]
- touchesEnd [touchA]
+ [self addGestureRecognizer: dtap];
+ [self addGestureRecognizer: stap];
+ [self addGestureRecognizer: pan];
+ [self addGestureRecognizer: pan2];
+ [self addGestureRecognizer: hold];
- Or even
+ [dtap release];
+ [stap release];
+ [pan release];
+ [pan2 release];
+ [hold release];
+}
- touchesBegan [touchA]
- touchesBegan [touchB]
- touchesEnd [touchA]
- touchesEnd [touchB]
- So the only way to properly detect a "pinch" gesture is to remember
- the start-point of each touch as it comes in; and the end-point of
- each touch as those come in; and only process the gesture once the
- number of touchEnds matches the number of touchBegins.
- */
- (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
{
- CGRect frame = [self bounds]; // Correct aspect ratio and scale.
+ // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
+ // Currently, this is the iPad Retina only.
+ CGRect frame = [self bounds]; // Scale.
double s = [self hackedContentScaleFactor];
*x *= (backbuffer_size.width / frame.size.width) / s;
*y *= (backbuffer_size.height / frame.size.height) / s;
}
-#if 0 // AudioToolbox/AudioToolbox.h
-- (void) beep
+/* Single click exits saver.
+ */
+- (void) handleTap
{
- // There's no way to play a standard system alert sound!
- // We'd have to include our own WAV for that. Eh, fuck it.
- AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
-# if TARGET_IPHONE_SIMULATOR
- NSLog(@"BEEP"); // The sim doesn't vibrate.
-# endif
+ [self stopAndClose:NO];
}
-#endif
-/* We distinguish between taps and drags.
- - Drags (down, motion, up) are sent to the saver to handle.
- - Single-taps exit the saver.
- This means a saver cannot respond to a single-tap. Only a few try to.
+/* Double click sends Space KeyPress.
*/
-
-- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
+- (void) handleDoubleTap
{
- // If they are trying to pinch, just do nothing.
- if ([[event allTouches] count] > 1)
- return;
+ if (!xsft->event_cb || !xwindow) return;
- tap_time = 0;
-
- if (xsft->event_cb && xwindow) {
- double s = [self hackedContentScaleFactor];
- XEvent xe;
- memset (&xe, 0, sizeof(xe));
- int i = 0;
- // #### 'frame' here or 'bounds'?
- int w = s * [self frame].size.width;
- int h = s * [self frame].size.height;
- for (UITouch *touch in touches) {
- CGPoint p = [touch locationInView:self];
- xe.xany.type = ButtonPress;
- xe.xbutton.button = i + 1;
- xe.xbutton.button = i + 1;
- xe.xbutton.x = s * p.x;
- xe.xbutton.y = s * p.y;
- [self rotateMouse: rot_current_angle
- x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
- jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
-
- // Ignore return code: don't care whether the hack handled it.
- xsft->event_cb (xdpy, xwindow, xdata, &xe);
-
- // Remember when/where this was, to determine tap versus drag or hold.
- tap_time = double_time();
- tap_point = p;
-
- i++;
- break; // No pinches: only look at the first touch.
- }
- }
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
+ xe.xkey.keycode = ' ';
+ xe.xany.type = KeyPress;
+ BOOL ok1 = [self sendEvent: &xe];
+ xe.xany.type = KeyRelease;
+ BOOL ok2 = [self sendEvent: &xe];
+ if (!(ok1 || ok2))
+ [self beep];
}
-- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
+/* Drag with one finger down: send MotionNotify.
+ */
+- (void) handlePan:(UIGestureRecognizer *)sender
{
- // If they are trying to pinch, just do nothing.
- if ([[event allTouches] count] > 1)
- return;
+ if (!xsft->event_cb || !xwindow) return;
- if (xsft->event_cb && xwindow) {
- double s = [self hackedContentScaleFactor];
- XEvent xe;
- memset (&xe, 0, sizeof(xe));
- int i = 0;
- // #### 'frame' here or 'bounds'?
- int w = s * [self frame].size.width;
- int h = s * [self frame].size.height;
- for (UITouch *touch in touches) {
- CGPoint p = [touch locationInView:self];
-
- // If the ButtonRelease came less than half a second after ButtonPress,
- // and didn't move far, then this was a tap, not a drag or a hold.
- // Interpret it as "exit".
- //
- double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
- ((p.y - tap_point.y) * (p.y - tap_point.y)));
- if (tap_time + 0.5 >= double_time() && dist < 20) {
- [self stopAndClose:NO];
- return;
- }
+ double s = [self hackedContentScaleFactor];
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
- xe.xany.type = ButtonRelease;
- xe.xbutton.button = i + 1;
- xe.xbutton.x = s * p.x;
- xe.xbutton.y = s * p.y;
- [self rotateMouse: rot_current_angle
- x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
- jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
- xsft->event_cb (xdpy, xwindow, xdata, &xe);
- i++;
- break; // No pinches: only look at the first touch.
- }
+ CGPoint p = [sender locationInView:self];
+ int x = s * p.x;
+ int y = s * p.y;
+ int w = s * [self frame].size.width; // #### 'frame' here or 'bounds'?
+ int h = s * [self frame].size.height;
+ [self rotateMouse: rot_current_angle x:&x y:&y w:w h:h];
+ jwxyz_mouse_moved (xdpy, xwindow, x, y);
+
+ switch (sender.state) {
+ case UIGestureRecognizerStateBegan:
+ xe.xany.type = ButtonPress;
+ xe.xbutton.button = 1;
+ xe.xbutton.x = x;
+ xe.xbutton.y = y;
+ break;
+
+ case UIGestureRecognizerStateEnded:
+ xe.xany.type = ButtonRelease;
+ xe.xbutton.button = 1;
+ xe.xbutton.x = x;
+ xe.xbutton.y = y;
+ break;
+
+ case UIGestureRecognizerStateChanged:
+ xe.xany.type = MotionNotify;
+ xe.xmotion.x = x;
+ xe.xmotion.y = y;
+ break;
+
+ default:
+ break;
}
+
+ BOOL ok = [self sendEvent: &xe];
+ if (!ok && xe.xany.type == ButtonRelease)
+ [self beep];
}
-- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
+/* Hold one finger down: assume we're about to start dragging.
+ Treat the same as Pan.
+ */
+- (void) handleLongPress:(UIGestureRecognizer *)sender
{
- // If they are trying to pinch, just do nothing.
- if ([[event allTouches] count] > 1)
+ [self handlePan:sender];
+}
+
+
+
+/* Drag with 2 fingers down: send arrow keys.
+ */
+- (void) handlePan2:(UIPanGestureRecognizer *)sender
+{
+ if (!xsft->event_cb || !xwindow) return;
+
+ if (sender.state != UIGestureRecognizerStateEnded)
return;
- if (xsft->event_cb && xwindow) {
- double s = [self hackedContentScaleFactor];
- XEvent xe;
- memset (&xe, 0, sizeof(xe));
- int i = 0;
- // #### 'frame' here or 'bounds'?
- int w = s * [self frame].size.width;
- int h = s * [self frame].size.height;
- for (UITouch *touch in touches) {
- CGPoint p = [touch locationInView:self];
- xe.xany.type = MotionNotify;
- xe.xmotion.x = s * p.x;
- xe.xmotion.y = s * p.y;
- [self rotateMouse: rot_current_angle
- x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
- jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
- xsft->event_cb (xdpy, xwindow, xdata, &xe);
- i++;
- break; // No pinches: only look at the first touch.
- }
- }
+ double s = [self hackedContentScaleFactor];
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
+
+ CGPoint p = [sender translationInView:self];
+ int x = s * p.x;
+ int y = s * p.y;
+ int w = s * [self frame].size.width; // #### 'frame' here or 'bounds'?
+ int h = s * [self frame].size.height;
+ [self rotateMouse: rot_current_angle x:&x y:&y w:w h:h];
+ // jwxyz_mouse_moved (xdpy, xwindow, x, y);
+
+ if (abs(x) > abs(y))
+ xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
+ else
+ xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
+
+ BOOL ok1 = [self sendEvent: &xe];
+ xe.xany.type = KeyRelease;
+ BOOL ok2 = [self sendEvent: &xe];
+ if (!(ok1 || ok2))
+ [self beep];
}
}
}
-
#endif // USE_IPHONE
+- (void) checkForUpdates
+{
+# ifndef USE_IPHONE
+ // We only check once at startup, even if there are multiple screens,
+ // and even if this saver is running for many days.
+ // (Uh, except this doesn't work because this static isn't shared,
+ // even if we make it an exported global. Not sure why. Oh well.)
+ static BOOL checked_p = NO;
+ if (checked_p) return;
+ checked_p = YES;
+
+ // If it's off, don't bother running the updater. Otherwise, the
+ // updater will decide if it's time to hit the network.
+ if (! get_boolean_resource (xdpy,
+ SUSUEnableAutomaticChecksKey,
+ SUSUEnableAutomaticChecksKey))
+ return;
+
+ NSString *updater = @"XScreenSaverUpdater.app";
+
+ // There may be multiple copies of the updater: e.g., one in /Applications
+ // and one in the mounted installer DMG! It's important that we run the
+ // one from the disk and not the DMG, so search for the right one.
+ //
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+ NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+ NSArray *search =
+ @[[[bundle bundlePath] stringByDeletingLastPathComponent],
+ [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
+ @"/Library/Screen Savers",
+ @"/System/Library/Screen Savers",
+ @"/Applications",
+ @"/Applications/Utilities"];
+ NSString *app_path = nil;
+ for (NSString *dir in search) {
+ NSString *p = [dir stringByAppendingPathComponent:updater];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
+ app_path = p;
+ break;
+ }
+ }
+
+ if (! app_path)
+ app_path = [workspace fullPathForApplication:updater];
+
+ if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
+ app_path = 0; // The DMG version will not do.
+
+ if (!app_path) {
+ NSLog(@"Unable to find %@", updater);
+ return;
+ }
+
+ NSError *err = nil;
+ if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
+ options:(NSWorkspaceLaunchWithoutAddingToRecents |
+ NSWorkspaceLaunchWithoutActivation |
+ NSWorkspaceLaunchAndHide)
+ configuration:nil
+ error:&err]) {
+ NSLog(@"Unable to launch %@: %@", app_path, err);
+ }
+
+# endif // !USE_IPHONE
+}
+
+
@end
/* Utility functions...