-/* xscreensaver, Copyright (c) 2006-2012 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-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.
+ */
/* 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"
+
/* Garbage collection only exists if we are being compiled against the
10.6 SDK or newer, not if we are building against the 10.4 SDK.
*/
# ifdef USE_IPHONE
+extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
+
/* Stub definition of the superclass, for iPhone.
*/
@implementation ScreenSaverView
+@interface XScreenSaverView (Private)
+- (void) stopAndClose:(Bool)relaunch;
+@end
+
@implementation XScreenSaverView
// Given a lower-cased saver name, returns the function table for it.
CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
CFRelease (url);
NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
+ // #### Analyze says "Potential leak of an object stored into cfb"
if (! name)
name = [[path lastPathComponent] stringByDeletingPathExtension];
- NSString *table_name = [[name lowercaseString]
- stringByAppendingString:
- @"_xscreensaver_function_table"];
+ name = [[name lowercaseString]
+ stringByReplacingOccurrencesOfString:@" "
+ withString:@""];
+
+# ifndef USE_IPHONE
+ // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
+ // I'm guessing that symbol-stripping is mandatory. Fuck.
+ NSString *table_name = [name stringByAppendingString:
+ @"_xscreensaver_function_table"];
void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
CFRelease (cfb);
if (! addr)
NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
+# else // USE_IPHONE
+ // Remember: any time you add a new saver to the iOS app,
+ // manually run "make ios-function-table.m"!
+ if (! function_tables)
+ function_tables = [make_function_table_dict() retain];
+ NSValue *v = [function_tables objectForKey: name];
+ void *addr = v ? [v pointerValue] : 0;
+# endif // USE_IPHONE
+
return (struct xscreensaver_function_table *) addr;
}
strcat (npath, opath);
if (putenv (npath)) {
perror ("putenv");
- abort();
+ NSAssert1 (0, @"putenv \"%s\" failed", npath);
}
/* Don't free (npath) -- MacOS's putenv() does not copy it. */
strcat (env, s);
if (putenv (env)) {
perror ("putenv");
- abort();
+ NSAssert1 (0, @"putenv \"%s\" failed", env);
}
/* Don't free (env) -- MacOS's putenv() does not copy it. */
}
{ "-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
// ".textLiteral: ",
// ".textFile: ",
- ".textURL: http://twitter.com/statuses/public_timeline.atom",
+ ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
// ".textProgram: ",
".grabDesktopImages: yes",
+# ifndef USE_IPHONE
".chooseRandomImages: no",
+# else
+ ".chooseRandomImages: yes",
+# 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].
"org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
*/
NSString *name = [NSString stringWithCString:xsft->progclass
- encoding:NSUTF8StringEncoding];
+ encoding:NSISOLatin1StringEncoding];
name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
[self setResourcesEnv:name];
next_frame_time = 0;
-# ifdef USE_IPHONE
+# ifdef USE_BACKBUFFER
[self createBackbuffer];
+ [self initLayer];
+# endif
+# ifdef USE_IPHONE
// So we can tell when we're docked.
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
# endif // USE_IPHONE
return self;
}
+- (void) initLayer
+{
+# if !defined(USE_IPHONE) && defined(USE_CALAYER)
+ [self setLayer: [CALayer layer]];
+ self.layer.delegate = self;
+ self.layer.opaque = YES;
+ [self setWantsLayer: YES];
+# endif // !USE_IPHONE && USE_CALAYER
+}
+
+
- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
{
return [self initWithFrame:frame saverName:0 isPreview:p];
if (xdpy)
jwxyz_free_display (xdpy);
-# ifdef USE_IPHONE
+# ifdef USE_BACKBUFFER
if (backbuffer)
CGContextRelease (backbuffer);
-# endif
+
+ if (colorspace)
+ CGColorSpaceRelease (colorspace);
+
+# ifndef USE_CALAYER
+ if (window_ctx)
+ CGContextRelease (window_ctx);
+# endif // !USE_CALAYER
+
+# endif // USE_BACKBUFFER
[prefsReader release];
// xsft
// fpst
- // orientation_timer
[super dealloc];
}
*/
- (void) allSystemsGo: (NSTimer *) timer
{
- NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
- [prefs setBool:YES forKey:@"wasRunning"];
NSAssert (timer == crash_timer, @"crash timer screwed up");
crash_timer = 0;
+
+ NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+ [prefs setBool:YES forKey:@"wasRunning"];
+ [prefs synchronize];
}
#endif // USE_IPHONE
# ifdef USE_IPHONE
if (crash_timer)
[crash_timer invalidate];
+
+ NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+ [prefs removeObjectForKey:@"wasRunning"];
+ [prefs synchronize];
+
crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
target:self
selector:@selector(allSystemsGo:)
userInfo:nil
repeats:NO];
+
# endif // USE_IPHONE
// Never automatically turn the screen off if we are docked,
# ifdef USE_IPHONE
[UIApplication sharedApplication].idleTimerDisabled =
([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
+ [[UIApplication sharedApplication]
+ setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
# endif
}
crash_timer = 0;
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs removeObjectForKey:@"wasRunning"];
+ [prefs synchronize];
# endif // USE_IPHONE
[super stopAnimation];
//
# ifdef USE_IPHONE
[UIApplication sharedApplication].idleTimerDisabled = NO;
+ [[UIApplication sharedApplication]
+ setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
# endif
}
fps_draw (fpst);
}
+
#ifdef USE_IPHONE
-/* Create a bitmap context into which we render everything.
+/* 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.)
+
+ This method is overridden in XScreenSaverGLView, since this kludge
+ isn't necessary for GL programs, being resolution independent by
+ nature.
*/
-- (void) createBackbuffer
+- (CGFloat) hackedContentScaleFactor
{
- CGContextRef ob = backbuffer;
- NSSize osize = backbuffer_size;
-
- CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
- double s = self.contentScaleFactor;
- backbuffer_size.width = (int) (s * rot_current_size.width);
- backbuffer_size.height = (int) (s * rot_current_size.height);
- backbuffer = CGBitmapContextCreate (NULL,
- backbuffer_size.width,
- backbuffer_size.height,
- 8,
- backbuffer_size.width * 4,
- cs,
- kCGImageAlphaPremultipliedLast);
- NSAssert (backbuffer, @"unable to allocate back buffer");
- CGColorSpaceRelease (cs);
-
- // Clear it.
- CGContextSetGrayFillColor (backbuffer, 0, 1);
- CGRect r = CGRectZero;
- r.size = backbuffer_size;
- CGContextFillRect (backbuffer, r);
-
- if (ob) {
- // Restore old bits, as much as possible, to the X11 upper left origin.
- NSRect rect;
- rect.origin.x = 0;
- rect.origin.y = (backbuffer_size.height - osize.height);
- rect.size = osize;
- CGImageRef img = CGBitmapContextCreateImage (ob);
- CGContextDrawImage (backbuffer, rect, img);
- CGImageRelease (img);
- CGContextRelease (ob);
- }
+ GLfloat s = [self contentScaleFactor];
+ if (initial_bounds.width >= 1024 ||
+ initial_bounds.height >= 1024)
+ s = 1;
+ return s;
}
+
static GLfloat _global_rot_current_angle_kludge;
double current_device_rotation (void)
# undef CLAMP180
- double s = self.contentScaleFactor;
- if (((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
- (int) backbuffer_size.height != (int) (s * rot_current_size.height))
-/* && rotation_ratio == -1*/)
- [self setFrame:[self frame]];
+ double s = [self hackedContentScaleFactor];
+ if (!ignore_rotation_p &&
+ /* rotation_ratio && */
+ ((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
+ (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
+ [self resize_x11];
+}
+
+
+- (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];
+ [self stopAnimation];
}
#endif // USE_IPHONE
+#ifdef USE_BACKBUFFER
+
+/* Create a bitmap context into which we render everything.
+ If the desired size has changed, re-created it.
+ */
+- (void) createBackbuffer
+{
+# ifdef USE_IPHONE
+ double s = [self hackedContentScaleFactor];
+ 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);
+# ifndef USE_CALAYER
+ if (window_ctx)
+ CGContextRelease (window_ctx);
+# endif
+
+ NSWindow *window = [self window];
+
+ if (window && xdpy) {
+ [self lockFocus];
+
+# ifndef USE_CALAYER
+ // 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");
+ }
+# else // USE_CALAYER
+ // Was apparently faster until 10.9.
+ colorspace = CGColorSpaceCreateDeviceRGB ();
+# endif // USE_CALAYER
+
+# ifndef USE_CALAYER
+ window_ctx = [[window graphicsContext] graphicsPort];
+ CGContextRetain (window_ctx);
+# endif // !USE_CALAYER
+
+ [self unlockFocus];
+ } else {
+# ifndef USE_CALAYER
+ window_ctx = NULL;
+# endif // !USE_CALAYER
+ colorspace = CGColorSpaceCreateDeviceRGB();
+ }
+
+ if (backbuffer &&
+ backbuffer_size.width == new_w &&
+ backbuffer_size.height == new_h)
+ return;
+
+ CGSize osize = backbuffer_size;
+ CGContextRef ob = backbuffer;
+
+ backbuffer_size.width = new_w;
+ backbuffer_size.height = new_h;
+
+ backbuffer = CGBitmapContextCreate (NULL,
+ backbuffer_size.width,
+ backbuffer_size.height,
+ 8,
+ backbuffer_size.width * 4,
+ colorspace,
+ // kCGImageAlphaPremultipliedLast
+ (kCGImageAlphaNoneSkipFirst |
+ kCGBitmapByteOrder32Host)
+ );
+ NSAssert (backbuffer, @"unable to allocate back buffer");
+
+ // Clear it.
+ CGRect r;
+ r.origin.x = r.origin.y = 0;
+ r.size = backbuffer_size;
+ CGContextSetGrayFillColor (backbuffer, 0, 1);
+ CGContextFillRect (backbuffer, r);
+
+ if (ob) {
+ // Restore old bits, as much as possible, to the X11 upper left origin.
+ CGRect rect;
+ rect.origin.x = 0;
+ rect.origin.y = (backbuffer_size.height - osize.height);
+ rect.size = osize;
+ CGImageRef img = CGBitmapContextCreateImage (ob);
+ CGContextDrawImage (backbuffer, rect, img);
+ CGImageRelease (img);
+ CGContextRelease (ob);
+ }
+}
+
+#endif // USE_BACKBUFFER
+
+
+/* Inform X11 that the size of our window has changed.
+ */
+- (void) resize_x11
+{
+ if (!xwindow) return; // early
+
+# ifdef USE_BACKBUFFER
+ [self createBackbuffer];
+ jwxyz_window_resized (xdpy, xwindow,
+ 0, 0,
+ backbuffer_size.width, backbuffer_size.height,
+ backbuffer);
+# else // !USE_BACKBUFFER
+ NSRect r = [self frame]; // ignoring rotation is closer
+ r.size = [self bounds].size; // to what XGetGeometry expects.
+ jwxyz_window_resized (xdpy, xwindow,
+ r.origin.x, r.origin.y,
+ r.size.width, r.size.height,
+ 0);
+# endif // !USE_BACKBUFFER
+
+ // Next time render_x11 is called, run the saver's reshape_cb.
+ resized_p = YES;
+}
+
+
- (void) render_x11
{
# ifdef USE_IPHONE
+ @try {
+
if (orientation == UIDeviceOrientationUnknown)
[self didRotate:nil];
[self hackRotation];
if (!initted_p) {
if (! xdpy) {
-# ifdef USE_IPHONE
+# ifdef USE_BACKBUFFER
NSAssert (backbuffer, @"no back buffer");
xdpy = jwxyz_make_display (self, backbuffer);
# else
xwindow = XRootWindow (xdpy, 0);
# ifdef USE_IPHONE
- jwxyz_window_resized (xdpy, xwindow,
- 0, 0,
- backbuffer_size.width, backbuffer_size.height,
- backbuffer);
-# else
- NSRect r = [self frame];
- jwxyz_window_resized (xdpy, xwindow,
- r.origin.x, r.origin.y,
- r.size.width, r.size.height,
- 0);
-# endif
+ /* 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];
}
if (!setup_p) {
initted_p = YES;
resized_p = NO;
NSAssert(!xdata, @"xdata already initialized");
-
+
+
# undef ya_rand_init
ya_rand_init (0);
if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
fpst = fps_init (xdpy, xwindow);
if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
+ } else {
+ xsft->fps_cb = 0;
}
+
+ [self checkForUpdates];
}
// Xlib drawing takes place under the animation timer.
[self resizeContext];
NSRect r;
-# ifndef USE_IPHONE
- r = [self frame];
-# else // USE_IPHONE
+# ifndef USE_BACKBUFFER
+ r = [self bounds];
+# else // USE_BACKBUFFER
r.origin.x = 0;
r.origin.y = 0;
r.size.width = backbuffer_size.width;
r.size.height = backbuffer_size.height;
-# endif // USE_IPHONE
+# endif // USE_BACKBUFFER
xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
resized_p = NO;
}
-
// Run any XtAppAddInput callbacks now.
// (Note that XtAppAddTimeOut callbacks have already been run by
// the Cocoa event loop.)
}
}
# endif // DO_GC_HACKERY
+
+# ifdef USE_IPHONE
+ }
+ @catch (NSException *e) {
+ [self handleException: e];
+ }
+# endif // USE_IPHONE
}
-/* On MacOS: drawRect does nothing, and animateOneFrame renders.
- On iOS GL: drawRect does nothing, and animateOneFrame renders.
- On iOS X11: drawRect renders, and animateOneFrame marks the view dirty.
+/* drawRect always does nothing, and animateOneFrame renders bits to the
+ screen. This is (now) true of both X11 and GL on both MacOS and iOS.
*/
-#ifndef USE_IPHONE
- (void)drawRect:(NSRect)rect
{
[super drawRect:rect]; // early: black.
}
+
+#ifndef USE_BACKBUFFER
+
- (void) animateOneFrame
{
[self render_x11];
+ jwxyz_flush_context(xdpy);
}
-#else // USE_IPHONE
+#else // USE_BACKBUFFER
-- (void)drawRect:(NSRect)rect
+- (void) animateOneFrame
{
// Render X11 into the backing store bitmap...
NSAssert (backbuffer, @"no back buffer");
- UIGraphicsPushContext (backbuffer);
- [self render_x11];
- UIGraphicsPopContext();
- // Then copy that bitmap to the screen.
+# ifdef USE_IPHONE
+ UIGraphicsPushContext (backbuffer);
+# endif
- CGContextRef cgc = UIGraphicsGetCurrentContext();
+ [self render_x11];
- // Mask it to only update the parts that are exposed.
-// CGContextClipToRect (cgc, rect);
+# ifdef USE_IPHONE
+ UIGraphicsPopContext();
+# endif
- double s = self.contentScaleFactor;
- CGRect frame = [self frame];
+# ifdef USE_IPHONE
+ // Then compute the transformations for rotation.
+ 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
+
+# ifdef USE_CALAYER
+ [self.layer setNeedsDisplay];
+# else // !USE_CALAYER
+ 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);
- NSRect target;
- target.size.width = backbuffer_size.width;
- target.size.height = backbuffer_size.height;
- target.origin.x = (s * frame.size.width - target.size.width) / 2;
- target.origin.y = (s * frame.size.height - target.size.height) / 2;
+ CGContextFlush (window_ctx);
+# endif // !USE_CALAYER
+}
- target.origin.x /= s;
- target.origin.y /= s;
- target.size.width /= s;
- target.size.height /= s;
+# ifdef USE_CALAYER
- CGAffineTransform t = CGAffineTransformIdentity;
+- (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);
- // Rotate around center
- float cx = frame.size.width / 2;
- float cy = frame.size.height / 2;
- t = CGAffineTransformTranslate (t, cx, cy);
- t = CGAffineTransformRotate (t, -rot_current_angle / (180.0 / M_PI));
- t = CGAffineTransformTranslate (t, -cx, -cy);
+ 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 {
- // Flip Y axis
- t = CGAffineTransformConcat (t,
- CGAffineTransformMake ( 1, 0, 0,
- -1, 0, frame.size.height));
+ // iPad 1: 9.6 ms, iPad 2: 12.1 ms
- // Clear background (visible in corners of screen during rotation)
- if (rotation_ratio != -1) {
- CGContextSetGrayFillColor (cgc, 0, 1);
- CGContextFillRect (cgc, frame);
+# 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);
}
-
- CGContextConcatCTM (cgc, t);
-
- // Copy the backbuffer to the screen.
- // Note that CGContextDrawImage measures in "points", not "pixels".
- CGImageRef img = CGBitmapContextCreateImage (backbuffer);
- CGContextDrawImage (cgc, target, img);
- CGImageRelease (img);
-}
-
-- (void) animateOneFrame
-{
- [self setNeedsDisplay];
}
+# endif // USE_CALAYER
-#endif // !USE_IPHONE
+#endif // USE_BACKBUFFER
{
[super setFrame:newRect];
-# ifdef USE_IPHONE
- [self createBackbuffer];
-# endif
-
- resized_p = YES; // The reshape_cb runs in render_x11
- if (xwindow) { // inform Xlib that the window has changed now.
-# ifdef USE_IPHONE
- NSAssert (backbuffer, @"no back buffer");
- // The backbuffer is the rotated size, and so is the xwindow.
- jwxyz_window_resized (xdpy, xwindow,
- 0, 0,
- backbuffer_size.width, backbuffer_size.height,
- backbuffer);
-# else
- jwxyz_window_resized (xdpy, xwindow,
- newRect.origin.x, newRect.origin.y,
- newRect.size.width, newRect.size.height,
- 0);
-# endif
- }
+ if (xwindow) // inform Xlib that the window has changed now.
+ [self resize_x11];
}
- (void) setFrameSize:(NSSize) newSize
{
[super setFrameSize:newSize];
- resized_p = YES;
if (xwindow)
- jwxyz_window_resized (xdpy, xwindow,
- [self frame].origin.x,
- [self frame].origin.y,
- newSize.width, newSize.height,
- 0); // backbuffer only on iPhone
+ [self resize_x11];
}
# endif // !USE_IPHONE
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 = data.length;
+ zs.next_out = (Bytef *) data2.bytes;
+ zs.avail_out = 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
{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *file = [NSString stringWithCString:xsft->progclass
- encoding:NSUTF8StringEncoding];
+ encoding:NSISOLatin1StringEncoding];
file = [file lowercaseString];
NSString *path = [bundle pathForResource:file ofType:@"xml"];
if (!path) {
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];
return sheet;
/* Announce our willingness to accept keyboard input.
-*/
+ */
- (BOOL)acceptsFirstResponder
{
return YES;
NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
toView:self];
# ifdef USE_IPHONE
- double s = self.contentScaleFactor;
+ double s = [self hackedContentScaleFactor];
# else
int s = 1;
# endif
int x = s * p.x;
- int y = s * ([self frame].size.height - p.y);
+ int y = s * ([self bounds].size.height - p.y);
xe.xany.type = type;
switch (type) {
}
}
+ if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
+
xe.xkey.keycode = k;
xe.xkey.state = state;
break;
}
default:
- abort();
+ NSAssert1 (0, @"unknown X11 event type: %d", type);
+ break;
}
[self lockFocus];
#else // USE_IPHONE
+- (void) stopAndClose:(Bool)relaunch_p
+{
+ if ([self isAnimating])
+ [self stopAnimation];
+
+ /* Need to make the SaverListController be the firstResponder again
+ so that it can continue to receive its own shake events. I
+ suppose that this abstraction-breakage means that I'm adding
+ XScreenSaverView to the UINavigationController wrong...
+ */
+ UIViewController *v = [[self window] rootViewController];
+ if ([v isKindOfClass: [UINavigationController class]]) {
+ UINavigationController *n = (UINavigationController *) v;
+ [[n topViewController] becomeFirstResponder];
+ }
+
+ UIView *fader = [self superview]; // the "backgroundView" view is our parent
+
+ if (relaunch_p) { // Fake a shake on the SaverListController.
+ // Why is [self window] sometimes null here?
+ UIWindow *w = [[UIApplication sharedApplication] keyWindow];
+ UIViewController *v = [w rootViewController];
+ if ([v isKindOfClass: [UINavigationController class]]) {
+ UINavigationController *n = (UINavigationController *) v;
+ [[n topViewController] motionEnded: UIEventSubtypeMotionShake
+ withEvent: nil];
+ }
+ } else { // Not launching another, animate our return to the list.
+ [UIView animateWithDuration: 0.5
+ animations:^{ fader.alpha = 0.0; }
+ completion:^(BOOL finished) {
+ [fader removeFromSuperview];
+ fader.alpha = 1.0;
+ }];
+ }
+}
+
+
/* Called after the device's orientation has changed.
Note: we could include a subclass of UIViewController which
Possibly XScreenSaverView should use Core Animation, and
XScreenSaverGLView should override that.
-*/
+ */
- (void)didRotate:(NSNotification *)notification
{
UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
if (rotation_ratio >= 0) return; // in the midst of rotation animation
if (orientation == current) return; // no change
+ // When transitioning to FaceUp or FaceDown, pretend there was no change.
+ if (current == UIDeviceOrientationFaceUp ||
+ current == UIDeviceOrientationFaceDown)
+ return;
+
new_orientation = current; // current animation target
rotation_ratio = 0; // start animating
rot_start_time = double_time();
switch (orientation) {
- case UIInterfaceOrientationLandscapeRight: angle_from = 90; break;
- case UIInterfaceOrientationLandscapeLeft: angle_from = 270; break;
- case UIInterfaceOrientationPortraitUpsideDown: angle_from = 180; break;
- default: angle_from = 0; break;
+ case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
+ case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
+ case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
+ default: angle_from = 0; break;
}
switch (new_orientation) {
- case UIInterfaceOrientationLandscapeRight: angle_to = 90; break;
- case UIInterfaceOrientationLandscapeLeft: angle_to = 270; break;
- case UIInterfaceOrientationPortraitUpsideDown: angle_to = 180; break;
- default: angle_to = 0; break;
+ case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
+ case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
+ case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
+ default: angle_to = 0; break;
}
- NSRect ff = [self frame];
-
switch (orientation) {
- case UIInterfaceOrientationLandscapeLeft: // from landscape
- case UIInterfaceOrientationLandscapeRight:
- rot_from.width = ff.size.height;
- rot_from.height = ff.size.width;
+ case UIDeviceOrientationLandscapeRight: // from landscape
+ case UIDeviceOrientationLandscapeLeft:
+ 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 UIInterfaceOrientationLandscapeLeft: // to landscape
- case UIInterfaceOrientationLandscapeRight:
- rot_to.width = ff.size.height;
- rot_to.height = ff.size.width;
+ case UIDeviceOrientationLandscapeRight: // to landscape
+ case UIDeviceOrientationLandscapeLeft:
+ 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;
}
}
-/* In the simulator, multi-touch sequences look like this:
+/* I believe we can't use UIGestureRecognizer for tracking touches
+ because UIPanGestureRecognizer doesn't give us enough detail in its
+ callbacks.
+
+ Currently we don't handle multi-touches (just the first touch) but
+ I'm leaving this comment here for future reference:
+
+ In the simulator, multi-touch sequences look like this:
touchesBegan [touchA, touchB]
touchesEnd [touchA, touchB]
number of touchEnds matches the number of touchBegins.
*/
-static void
-rotate_mouse (int *x, int *y, int w, int h, int rot)
+- (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
{
- int ox = *x, oy = *y;
- if (rot > 45 && rot < 135) { *x = oy; *y = w-ox; }
- else if (rot < -45 && rot > -135) { *x = h-oy; *y = ox; }
- else if (rot > 135 || rot < -135) { *x = w-ox; *y = h-oy; }
+ // 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
+{
+ // 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
}
+#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.
+ */
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
+ // If they are trying to pinch, just do nothing.
+ if ([[event allTouches] count] > 1)
+ return;
+
+ tap_time = 0;
+
if (xsft->event_cb && xwindow) {
- double s = self.contentScaleFactor;
+ 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) {
xe.xbutton.button = i + 1;
xe.xbutton.x = s * p.x;
xe.xbutton.y = s * p.y;
- rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
+ [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.
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
-
- // Double-tap means "exit" and return to selection menu.
- //
- for (UITouch *touch in touches) {
- if ([touch tapCount] >= 2) {
- if ([self isAnimating])
- [self stopAnimation];
- [self removeFromSuperview];
- return;
- }
- }
+ // If they are trying to pinch, just do nothing.
+ if ([[event allTouches] count] > 1)
+ return;
if (xsft->event_cb && xwindow) {
- double s = self.contentScaleFactor;
+ 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;
+ }
+
xe.xany.type = ButtonRelease;
xe.xbutton.button = i + 1;
xe.xbutton.x = s * p.x;
xe.xbutton.y = s * p.y;
- rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
+ [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.
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
+ // If they are trying to pinch, just do nothing.
+ if ([[event allTouches] count] > 1)
+ return;
+
if (xsft->event_cb && xwindow) {
- double s = self.contentScaleFactor;
+ 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) {
xe.xany.type = MotionNotify;
xe.xmotion.x = s * p.x;
xe.xmotion.y = s * p.y;
- rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
+ [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.
}
}
}
/* We need this to respond to "shake" gestures
*/
-- (BOOL)canBecomeFirstResponder {
+- (BOOL)canBecomeFirstResponder
+{
return YES;
}
+- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+
+- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+/* Shake means exit and launch a new saver.
+ */
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+ [self stopAndClose:YES];
+}
+
- (void)setScreenLocked:(BOOL)locked
{
}
}
-
#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...
get_prefsReader (Display *dpy)
{
XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
- if (!view) abort();
+ if (!view) return 0;
return [view prefsReader];
}