X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=OSX%2FXScreenSaverView.m;h=602629533f4c691cb90e5bc904b5cf08a9cf34a0;hb=7edd66e6bd3209013ee059819747b10b5835635b;hp=528c4f6c5b0a36e3032445aea201d4a230a35957;hpb=f0261d8acab611f3433160e4f07367b870439739;p=xscreensaver diff --git a/OSX/XScreenSaverView.m b/OSX/XScreenSaverView.m index 528c4f6c..60262953 100644 --- a/OSX/XScreenSaverView.m +++ b/OSX/XScreenSaverView.m @@ -1,13 +1,13 @@ -/* xscreensaver, Copyright (c) 2006, 2007 Jamie Zawinski -* -* 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 + * + * 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" @@ -15,12 +15,27 @@ the "screenhack.c" module. */ +#import +#import #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. + */ +#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 */ +# import +# define DO_GC_HACKERY +#endif + extern struct xscreensaver_function_table *xscreensaver_function_table; /* Global variables used by the screen savers @@ -30,19 +45,74 @@ const char *progclass; int mono_p = 0; +# ifdef USE_IPHONE + +# define NSSizeToCGSize(x) (x) + +extern NSDictionary *make_function_table_dict(void); // ios-function-table.m + +/* Stub definition of the superclass, for iPhone. + */ +@implementation ScreenSaverView +{ + NSTimeInterval anim_interval; + Bool animating_p; + NSTimer *anim_timer; +} + +- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview { + self = [super initWithFrame:frame]; + if (! self) return 0; + anim_interval = 1.0/30; + return self; +} +- (NSTimeInterval)animationTimeInterval { return anim_interval; } +- (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; } +- (BOOL)hasConfigureSheet { return NO; } +- (NSWindow *)configureSheet { return nil; } +- (NSView *)configureView { return nil; } +- (BOOL)isPreview { return NO; } +- (BOOL)isAnimating { return animating_p; } +- (void)animateOneFrame { } + +- (void)startAnimation { + if (animating_p) return; + animating_p = YES; + anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval + target:self + selector:@selector(animateOneFrame) + userInfo:nil + repeats:YES]; +} + +- (void)stopAnimation { + if (anim_timer) { + [anim_timer invalidate]; + anim_timer = 0; + } + animating_p = NO; +} +@end + +# endif // !USE_IPHONE + + + +@interface XScreenSaverView (Private) +- (void) stopAndClose:(Bool)relaunch; +@end + @implementation XScreenSaverView -- (struct xscreensaver_function_table *) findFunctionTable +// Given a lower-cased saver name, returns the function table for it. +// If no name, guess the name from the class's bundle name. +// +- (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name { NSBundle *nsb = [NSBundle bundleForClass:[self class]]; NSAssert1 (nsb, @"no bundle for class %@", [self class]); - + NSString *path = [nsb bundlePath]; - NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension] - lowercaseString]; - NSString *suffix = @"_xscreensaver_function_table"; - NSString *table_name = [name stringByAppendingString:suffix]; - CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef) path, kCFURLPOSIXPathStyle, @@ -50,11 +120,34 @@ int mono_p = 0; 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]; + + 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); - NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path); + CFRelease (cfb); + + if (! addr) + NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path); + +# else // USE_IPHONE + // Depends on the auto-generated "ios-function-table.m" being up to date. + 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 -// NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr); return (struct xscreensaver_function_table *) addr; } @@ -79,9 +172,10 @@ int mono_p = 0; strcat (npath, opath); if (putenv (npath)) { perror ("putenv"); - abort(); + NSAssert1 (0, @"putenv \"%s\" failed", npath); } -// free (npath); // Oops, don't free this! putenv() does not copy it! + + /* Don't free (npath) -- MacOS's putenv() does not copy it. */ } @@ -99,9 +193,9 @@ int mono_p = 0; strcat (env, s); if (putenv (env)) { perror ("putenv"); - abort(); + NSAssert1 (0, @"putenv \"%s\" failed", env); } - free (env); + /* Don't free (env) -- MacOS's putenv() does not copy it. */ } @@ -114,13 +208,14 @@ add_default_options (const XrmOptionDescRec *opts, /* These aren't "real" command-line options (there are no actual command-line options in the Cocoa version); but this is the somewhat kludgey way that the and tags in the - ../hacks/config/*.xml files communicate with the preferences database. + ../hacks/config/\*.xml files communicate with the preferences database. */ static const XrmOptionDescRec default_options [] = { { "-text-mode", ".textMode", XrmoptionSepArg, 0 }, { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 }, { "-text-file", ".textFile", XrmoptionSepArg, 0 }, { "-text-url", ".textURL", XrmoptionSepArg, 0 }, + { "-text-program", ".textProgram", XrmoptionSepArg, 0 }, { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" }, { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"}, { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" }, @@ -128,17 +223,68 @@ add_default_options (const XrmOptionDescRec *opts, { "-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 + // + { "-" 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 [] = { ".doFPS: False", + ".doubleBuffer: True", + ".multiSample: False", +# ifndef USE_IPHONE ".textMode: date", +# else + ".textMode: url", +# endif // ".textLiteral: ", // ".textFile: ", - // ".textURL: ", + ".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", + ".texFontCacheSize: 30", + +# 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 }; @@ -192,12 +338,55 @@ add_default_options (const XrmOptionDescRec *opts, } -- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview +#ifdef USE_IPHONE +/* Returns the current time in seconds as a double. + */ +static double +double_time (void) +{ + struct timeval now; +# ifdef GETTIMEOFDAY_TWO_ARGS + struct timezone tzp; + gettimeofday(&now, &tzp); +# else + gettimeofday(&now); +# endif + + return (now.tv_sec + ((double) now.tv_usec * 0.000001)); +} +#endif // USE_IPHONE + +#if TARGET_IPHONE_SIMULATOR +static const char * +orientname(unsigned long o) +{ + switch (o) { + case UIDeviceOrientationUnknown: return "Unknown"; + case UIDeviceOrientationPortrait: return "Portrait"; + case UIDeviceOrientationPortraitUpsideDown: return "PortraitUpsideDown"; + case UIDeviceOrientationLandscapeLeft: return "LandscapeLeft"; + case UIDeviceOrientationLandscapeRight: return "LandscapeRight"; + case UIDeviceOrientationFaceUp: return "FaceUp"; + case UIDeviceOrientationFaceDown: return "FaceDown"; + default: return "ERROR"; + } +} +#endif // TARGET_IPHONE_SIMULATOR + + +- (id) initWithFrame:(NSRect)frame + saverName:(NSString *)saverName + isPreview:(BOOL)isPreview { if (! (self = [super initWithFrame:frame isPreview:isPreview])) return 0; - xsft = [self findFunctionTable]; + xsft = [self findFunctionTable: saverName]; + if (! xsft) { + [self release]; + return 0; + } + [self setShellPath]; setup_p = YES; @@ -209,7 +398,7 @@ add_default_options (const XrmOptionDescRec *opts, "org.jwz.xscreensaver...plist" */ NSString *name = [NSString stringWithCString:xsft->progclass - encoding:NSUTF8StringEncoding]; + encoding:NSISOLatin1StringEncoding]; name = [@"org.jwz.xscreensaver." stringByAppendingString:name]; [self setResourcesEnv:name]; @@ -226,17 +415,79 @@ add_default_options (const XrmOptionDescRec *opts, progname = progclass = xsft->progclass; next_frame_time = 0; - + +# ifdef USE_IPHONE + double s = [self hackedContentScaleFactor]; +# else + double s = 1; +# endif + + CGSize bb_size; // pixels, not points + bb_size.width = s * frame.size.width; + bb_size.height = s * frame.size.height; + +# ifdef USE_IPHONE + initial_bounds = rot_current_size = rot_from = rot_to = bb_size; + rotation_ratio = -1; + + orientation = UIDeviceOrientationUnknown; + [self didRotate:nil]; + [self initGestures]; + + // So we can tell when we're docked. + [UIDevice currentDevice].batteryMonitoringEnabled = YES; +# endif // USE_IPHONE + +# ifdef USE_BACKBUFFER + [self createBackbuffer:bb_size]; + [self initLayer]; +# endif + return self; } +- (void) initLayer +{ +# if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER) + [self setLayer: [CALayer layer]]; + self.layer.delegate = self; + self.layer.opaque = YES; + [self setWantsLayer: YES]; +# endif // !USE_IPHONE && BACKBUFFER_CALAYER +} + + +- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p +{ + return [self initWithFrame:frame saverName:0 isPreview:p]; +} + + - (void) dealloc { 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); + + if (colorspace) + CGColorSpaceRelease (colorspace); + +# ifdef BACKBUFFER_CGCONTEXT + if (window_ctx) + CGContextRelease (window_ctx); +# endif // BACKBUFFER_CGCONTEXT + +# endif // USE_BACKBUFFER + [prefsReader release]; + + // xsft + // fpst + [super dealloc]; } @@ -246,17 +497,72 @@ add_default_options (const XrmOptionDescRec *opts, } +#ifdef USE_IPHONE +- (void) lockFocus { } +- (void) unlockFocus { } +#endif // USE_IPHONE + + + +# ifdef USE_IPHONE +/* A few seconds after the saver launches, we store the "wasRunning" + preference. This is so that if the saver is crashing at startup, + we don't launch it again next time, getting stuck in a crash loop. + */ +- (void) allSystemsGo: (NSTimer *) timer +{ + 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 + + - (void) startAnimation { 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 to animateOneFrame() instead. */ + +# 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, + // and an animation is running. + // +# ifdef USE_IPHONE + [UIApplication sharedApplication].idleTimerDisabled = + ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged); + [[UIApplication sharedApplication] + setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone]; +# endif } + - (void)stopAnimation { NSAssert([self isAnimating], @"not animating"); @@ -275,12 +581,36 @@ add_default_options (const XrmOptionDescRec *opts, 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 + if (crash_timer) + [crash_timer invalidate]; + crash_timer = 0; + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + [prefs removeObjectForKey:@"wasRunning"]; + [prefs synchronize]; +# endif // USE_IPHONE + [super stopAnimation]; + + // When an animation is no longer running (e.g., looking at the list) + // then it's ok to power off the screen when docked. + // +# ifdef USE_IPHONE + [UIApplication sharedApplication].idleTimerDisabled = NO; + [[UIApplication sharedApplication] + setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone]; +# endif } @@ -300,18 +630,336 @@ add_default_options (const XrmOptionDescRec *opts, static void screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) { - fps_compute (fpst, 0); + fps_compute (fpst, 0, -1); fps_draw (fpst); } -- (void) animateOneFrame +#ifdef USE_IPHONE + +/* 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. + */ +- (CGFloat) hackedContentScaleFactor +{ + NSSize ssize = [[[UIScreen mainScreen] currentMode] size]; + NSSize bsize = [self bounds].size; + + // Ratio of screen size in pixels to view size in points. + GLfloat s = ((ssize.width > ssize.height ? ssize.width : ssize.height) / + (bsize.width > bsize.height ? bsize.width : bsize.height)); + + if (ssize.width >= 1024 && ssize.height >= 1024) + s = 1; + + return s; +} + + +static GLfloat _global_rot_current_angle_kludge; + +double current_device_rotation (void) +{ + return -_global_rot_current_angle_kludge; +} + + +- (void) hackRotation +{ + if (rotation_ratio >= 0) { // in the midst of a rotation animation + +# define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360 + GLfloat f = angle_from; + GLfloat t = angle_to; + CLAMP180(f); + CLAMP180(t); + GLfloat dist = -(t-f); + CLAMP180(dist); + + // Intermediate angle. + rot_current_angle = f - rotation_ratio * dist; + + // Intermediate frame size. + rot_current_size.width = rot_from.width + + rotation_ratio * (rot_to.width - rot_from.width); + rot_current_size.height = rot_from.height + + rotation_ratio * (rot_to.height - rot_from.height); + + // Tick animation. Complete rotation in 1/6th sec. + double now = double_time(); + double duration = 1/6.0; + rotation_ratio = 1 - ((rot_start_time + duration - now) / duration); + + if (rotation_ratio > 1 || ignore_rotation_p) { // Done animating. + orientation = new_orientation; + rot_current_angle = angle_to; + rot_current_size = rot_to; + rotation_ratio = -1; + +# if TARGET_IPHONE_SIMULATOR + NSLog (@"rotation ended: %s %d, %d x %d", + orientname(orientation), (int) rot_current_angle, + (int) rot_current_size.width, (int) rot_current_size.height); +# endif + + // Check orientation again in case we rotated again while rotating: + // this is a no-op if nothing has changed. + [self didRotate:nil]; + } + } else { // Not animating a rotation. + rot_current_angle = angle_to; + rot_current_size = rot_to; + } + + CLAMP180(rot_current_angle); + _global_rot_current_angle_kludge = rot_current_angle; + +# undef CLAMP180 + + CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow]) + ? initial_bounds + : rot_current_size); + if ((int) backbuffer_size.width != (int) rotsize.width || + (int) backbuffer_size.height != (int) rotsize.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. + new_size is in rotated pixels, not points: the same size + and shape as the X11 window as seen by the hacks. + */ +- (void) createBackbuffer:(CGSize)new_size +{ + // 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 && + (int)backbuffer_size.width == (int)new_size.width && + (int)backbuffer_size.height == (int)new_size.height) + return; + + CGContextRef ob = backbuffer; + + CGSize osize = backbuffer_size; // pixels, not points. + backbuffer_size = new_size; // pixels, not points. + +# if TARGET_IPHONE_SIMULATOR + NSLog(@"backbuffer %.0fx%.0f", + backbuffer_size.width, backbuffer_size.height); +# endif + + backbuffer = CGBitmapContextCreate (NULL, + (int)backbuffer_size.width, + (int)backbuffer_size.height, + 8, + (int)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; // pixels, not points + 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 + + CGSize new_size; // pixels, not points + +# ifdef USE_BACKBUFFER +# ifdef USE_IPHONE + CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow]) + ? initial_bounds + : rot_current_size); + new_size.width = rotsize.width; + new_size.height = rotsize.height; +# else // !USE_IPHONE + new_size = NSSizeToCGSize([self bounds].size); +# endif // !USE_IPHONE + + [self createBackbuffer:new_size]; + jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height, + backbuffer); +# else // !USE_BACKBUFFER + new_size = [self bounds].size; + jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height, + 0); +# endif // !USE_BACKBUFFER + +# if TARGET_IPHONE_SIMULATOR + NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height); +# endif + + // 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]; +# endif + if (!initted_p) { if (! xdpy) { - xdpy = jwxyz_make_display (self); +# ifdef USE_BACKBUFFER + NSAssert (backbuffer, @"no back buffer"); + xdpy = jwxyz_make_display (self, backbuffer); +# else + 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]; } if (!setup_p) { @@ -322,7 +970,8 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) initted_p = YES; resized_p = NO; NSAssert(!xdata, @"xdata already initialized"); - + + # undef ya_rand_init ya_rand_init (0); @@ -331,7 +980,9 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) "background", "Background")); XClearWindow (xdpy, xwindow); +# ifndef USE_IPHONE [[self window] setAcceptsMouseMovedEvents:YES]; +# endif /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz drawing primitives will run on the GPU instead of the CPU. @@ -352,18 +1003,28 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) (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]; } + /* I don't understand why we have to do this *every frame*, but we do, or else the cursor comes back on. */ +# ifndef USE_IPHONE if (![self isPreview]) [NSCursor setHiddenUntilMouseMoves:YES]; +# endif if (fpst) @@ -371,30 +1032,46 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) /* This is just a guess, but the -fps code wants to know how long we were sleeping between frames. */ - unsigned long usecs = 1000000 * [self animationTimeInterval]; + long usecs = 1000000 * [self animationTimeInterval]; usecs -= 200; // caller apparently sleeps for slightly less sometimes... + if (usecs < 0) usecs = 0; fps_slept (fpst, usecs); } - /* 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); @@ -404,10 +1081,19 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) [self prepareContext]; if (resized_p) { - // We do this here instead of in setFrameSize so that all the + // We do this here instead of in setFrame so that all the // Xlib drawing takes place under the animation timer. [self resizeContext]; - NSRect r = [self frame]; + NSRect r; +# 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_BACKBUFFER + xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height); resized_p = NO; } @@ -418,20 +1104,72 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) // jwxyz_sources_run (display_sources_data (xdpy)); + // And finally: // +# 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); - XSync (xdpy, 0); +# ifndef USE_IPHONE NSEnableScreenUpdates(); +# endif gettimeofday (&tv, 0); now = tv.tv_sec + (tv.tv_usec / 1000000.0); next_frame_time = now + (delay / 1000000.0); + +# ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt. + if (delay < [self animationTimeInterval]) + [self setAnimationTimeInterval:(delay / 1000000.0)]; +# endif + +# ifdef DO_GC_HACKERY + /* Current theory is that the 10.6 garbage collector sucks in the + following way: + + It only does a collection when a threshold of outstanding + collectable allocations has been surpassed. However, CoreGraphics + creates lots of small collectable allocations that contain pointers + to very large non-collectable allocations: a small CG object that's + collectable referencing large malloc'd allocations (non-collectable) + containing bitmap data. So the large allocation doesn't get freed + until GC collects the small allocation, which triggers its finalizer + to run which frees the large allocation. So GC is deciding that it + doesn't really need to run, even though the process has gotten + enormous. GC eventually runs once pageouts have happened, but by + then it's too late, and the machine's resident set has been + sodomized. + + So, we force an exhaustive garbage collection in this process + approximately every 5 seconds whether the system thinks it needs + one or not. + */ + { + static int tick = 0; + if (++tick > 5*30) { + tick = 0; + objc_collect (OBJC_EXHAUSTIVE_COLLECTION); + } + } +# endif // DO_GC_HACKERY + +# ifdef USE_IPHONE + } + @catch (NSException *e) { + [self handleException: e]; + } +# endif // USE_IPHONE } +/* 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. + */ + - (void)drawRect:(NSRect)rect { if (xwindow) // clear to the X window's bg color, not necessarily black. @@ -441,22 +1179,174 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) } -- (void) setFrameSize:(NSSize) newSize +#ifndef USE_BACKBUFFER + +- (void) animateOneFrame { - [super setFrameSize:newSize]; - if ([self isAnimating]) { - resized_p = YES; + [self render_x11]; + jwxyz_flush_context(xdpy); +} + +#else // USE_BACKBUFFER + +- (void) animateOneFrame +{ + // Render X11 into the backing store bitmap... + + NSAssert (backbuffer, @"no back buffer"); + +# ifdef USE_IPHONE + UIGraphicsPushContext (backbuffer); +# endif + + [self render_x11]; + +# ifdef USE_IPHONE + UIGraphicsPopContext(); +# endif + +# ifdef USE_IPHONE + // 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)); + + // Ratio of backbuffer size in pixels to layer size in points. + CGSize ssize = backbuffer_size; + CGSize bsize = [self bounds].size; + GLfloat s = ((ssize.width > ssize.height ? ssize.width : ssize.height) / + (bsize.width > bsize.height ? bsize.width : bsize.height)); + + self.layer.contentsScale = s; + self.layer.affineTransform = t; + + /* Setting the layer's bounds also sets the view's bounds. + The view's bounds must be in points, not pixels, and it + must be rotated to the current orientation. + */ + CGRect bounds; + bounds.origin.x = 0; + bounds.origin.y = 0; + bounds.size.width = ssize.width / s; + bounds.size.height = ssize.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); + + CGContextFlush (window_ctx); +# endif // BACKBUFFER_CGCONTEXT +} + +# 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 + + - (void) setFrame:(NSRect) newRect { [super setFrame:newRect]; - if (xwindow) // inform Xlib that the window has changed. - jwxyz_window_resized (xdpy, xwindow); + + if (xwindow) // inform Xlib that the window has changed now. + [self resize_x11]; } +# ifndef USE_IPHONE // Doesn't exist on iOS +- (void) setFrameSize:(NSSize) newSize +{ + [super setFrameSize:newSize]; + if (xwindow) + [self resize_x11]; +} +# endif // !USE_IPHONE + + +(BOOL) performGammaFade { return YES; @@ -467,11 +1357,49 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) return YES; } ++ (NSString *) decompressXML: (NSData *)data +{ + if (! data) return 0; + BOOL compressed_p = !!strncmp ((const char *) data.bytes, "")); + } + + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + + +#ifndef USE_IPHONE - (NSWindow *) configureSheet +#else +- (UIViewController *) configureView +#endif { 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) { @@ -480,38 +1408,93 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) return nil; } - NSWindow *sheet = [[XScreenSaverConfigSheet alloc] - initWithXMLFile:path - options:xsft->options - controller:[prefsReader userDefaultsController]]; - +# ifdef USE_IPHONE + UIViewController *sheet; +# else // !USE_IPHONE + NSWindow *sheet; +# endif // !USE_IPHONE + + NSData *xmld = [NSData dataWithContentsOfFile:path]; + NSString *xml = [[self class] decompressXML: xmld]; + sheet = [[XScreenSaverConfigSheet alloc] + 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. - [sheet retain]; + // #### Analyze says "potential leak of an object stored into sheet" + // [sheet retain]; return sheet; } +- (NSUserDefaultsController *) userDefaultsController +{ + return [prefsReader userDefaultsController]; +} + + /* 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 + // 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)); @@ -526,9 +1509,14 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow] toView:self]; - int x = p.x; - int y = [self frame].size.height - p.y; - +# 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); + xe.xany.type = type; switch (type) { case ButtonPress: @@ -553,84 +1541,661 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) case KeyPress: case KeyRelease: { - NSString *nss = [e characters]; - const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding]; - xe.xkey.keycode = (s && *s ? *s : 0); + NSString *ns = (([e type] == NSFlagsChanged) ? 0 : + [e charactersIgnoringModifiers]); + KeySym k = 0; + + if (!ns || [ns length] == 0) // dead key + { + // Cocoa hides the difference between left and right keys. + // Also we only get KeyPress events for these, no KeyRelease + // (unless we hack the mod state manually. Bleh.) + // + if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock; + else if (flags & NSShiftKeyMask) k = XK_Shift_L; + else if (flags & NSControlKeyMask) k = XK_Control_L; + else if (flags & NSAlternateKeyMask) k = XK_Alt_L; + else if (flags & NSCommandKeyMask) k = XK_Meta_L; + } + else if ([ns length] == 1) // real key + { + switch ([ns characterAtIndex:0]) { + case NSLeftArrowFunctionKey: k = XK_Left; break; + case NSRightArrowFunctionKey: k = XK_Right; break; + case NSUpArrowFunctionKey: k = XK_Up; break; + case NSDownArrowFunctionKey: k = XK_Down; break; + case NSPageUpFunctionKey: k = XK_Page_Up; break; + case NSPageDownFunctionKey: k = XK_Page_Down; break; + case NSHomeFunctionKey: k = XK_Home; break; + case NSPrevFunctionKey: k = XK_Prior; break; + case NSNextFunctionKey: k = XK_Next; break; + case NSBeginFunctionKey: k = XK_Begin; break; + case NSEndFunctionKey: k = XK_End; break; + default: + { + const char *s = + [ns cStringUsingEncoding:NSISOLatin1StringEncoding]; + k = (s && *s ? *s : 0); + } + break; + } + } + + 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]; - [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 convertEvent:e type:KeyPress]) + [super flagsChanged:e]; +} + +#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]; +// } + [self resignFirstResponder]; + + // Find SaverRunner.window (as opposed to SaverRunner.saverWindow) + UIWindow *listWindow = 0; + for (UIWindow *w in [[UIApplication sharedApplication] windows]) { + if (w != [self window]) { + listWindow = w; + break; + } + } + + UIView *fader = [self superview]; // the "backgroundView" view is our parent + + if (relaunch_p) { // Fake a shake on the SaverListController. + UIViewController *v = [listWindow rootViewController]; + if ([v isKindOfClass: [UINavigationController class]]) { +# if TARGET_IPHONE_SIMULATOR + NSLog (@"simulating shake on saver list"); +# endif + UINavigationController *n = (UINavigationController *) v; + [[n topViewController] motionEnded: UIEventSubtypeMotionShake + withEvent: nil]; + } + } else { // Not launching another, animate our return to the list. +# if TARGET_IPHONE_SIMULATOR + NSLog (@"fading back to saver list"); +# endif + UIWindow *saverWindow = [self window]; // not SaverRunner.window + [listWindow setHidden:NO]; + [UIView animateWithDuration: 0.5 + animations:^{ fader.alpha = 0.0; } + completion:^(BOOL finished) { + [fader removeFromSuperview]; + fader.alpha = 1.0; + [saverWindow setHidden:YES]; + [listWindow makeKeyAndVisible]; + [[[listWindow rootViewController] view] becomeFirstResponder]; + }]; + } +} + + +/* Whether the shape of the X11 Window should be changed to HxW when the + device is in a landscape orientation. X11 hacks want this, but OpenGL + hacks do not. + */ +- (BOOL)reshapeRotatedWindow +{ + return YES; +} + + +/* Called after the device's orientation has changed. + + Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways. + + The UI (list of savers, preferences panels) is rotated by the system, + because its UIWindow is under a UINavigationController that does + automatic rotation, using Core Animation. + + The savers are under a different UIWindow and a UINavigationController + that does not do automatic rotation. + + We have to do it this way for OpenGL savers because using Core Animation + on an EAGLContext causes the OpenGL pipeline to fall back on software + rendering and performance goes to hell. + + For X11-only savers, we could just use Core Animation and let the system + handle it, but (maybe) it's simpler to do it the same way for X11 and GL. + + During and after rotation, the size/shape of the X11 window changes, + and ConfigureNotify events are generated. + + X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which + rotated at the last minute via a CGAffineTransformMakeRotation when it is + copied to the display hardware. + + GL code always recieves a portrait-oriented X11 Window whose size never + changes. The GL COLOR_BUFFER is displayed on the hardware directly and + unrotated, so the GL hacks themselves are responsible for rotating the + GL scene to match current_device_rotation(). + + Touch events are converted to mouse clicks, and those XEvent coordinates + are reported in the coordinate system currently in use by the X11 window. + Again, GL must convert those. + */ +- (void)didRotate:(NSNotification *)notification +{ + UIDeviceOrientation current = [[UIDevice currentDevice] orientation]; + + /* If the simulator starts up in the rotated position, sometimes + the UIDevice says we're in Portrait when we're not -- but it + turns out that the UINavigationController knows what's up! + So get it from there. + */ + if (current == UIDeviceOrientationUnknown) { + switch ([[[self window] rootViewController] interfaceOrientation]) { + case UIInterfaceOrientationPortrait: + current = UIDeviceOrientationPortrait; + break; + case UIInterfaceOrientationPortraitUpsideDown: + current = UIDeviceOrientationPortraitUpsideDown; + break; + /* It's opposite day, "because rotating the device to the left requires + rotating the content to the right" */ + case UIInterfaceOrientationLandscapeLeft: + current = UIDeviceOrientationLandscapeRight; + break; + case UIInterfaceOrientationLandscapeRight: + current = UIDeviceOrientationLandscapeLeft; + break; + default: + break; + } + } + + /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get + an orientation change event with an unknown orientation. Those seem + to always be immediately followed by another orientation change with + a *real* orientation change, so let's try just ignoring those bogus + ones and hoping that the real one comes in shortly... + */ + if (current == UIDeviceOrientationUnknown) + return; + + 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 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 UIDeviceOrientationLandscapeLeft: angle_to = 90; break; + case UIDeviceOrientationLandscapeRight: angle_to = 270; break; + case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break; + default: angle_to = 0; break; + } + + switch (orientation) { + 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 = initial_bounds.width; + rot_from.height = initial_bounds.height; + break; + } + + switch (new_orientation) { + 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 = initial_bounds.width; + rot_to.height = initial_bounds.height; + break; + } + +# if TARGET_IPHONE_SIMULATOR + NSLog (@"rotation begun: %s %d -> %s %d; %d x %d", + orientname(orientation), (int) rot_current_angle, + orientname(new_orientation), (int) angle_to, + (int) rot_current_size.width, (int) rot_current_size.height); +# endif + + if (! initted_p) { + // If we've done a rotation but the saver hasn't been initialized yet, + // don't bother going through an X11 resize, but just do it now. + rot_start_time = 0; // dawn of time + [self hackRotation]; + } +} + + +/* We distinguish between taps and drags. + + - 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. + + This means a saver cannot respond to a single-tap. Only a few try to. + */ + +- (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 */ + + [stap requireGestureRecognizerToFail: dtap]; + [stap requireGestureRecognizerToFail: hold]; + [dtap requireGestureRecognizerToFail: hold]; + [pan requireGestureRecognizerToFail: hold]; + + [self setMultipleTouchEnabled:YES]; + + [self addGestureRecognizer: dtap]; + [self addGestureRecognizer: stap]; + [self addGestureRecognizer: pan]; + [self addGestureRecognizer: pan2]; + [self addGestureRecognizer: hold]; + + [dtap release]; + [stap release]; + [pan release]; + [pan2 release]; + [hold release]; +} + + +/* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates, + convert it to what X11 and OpenGL expect. + */ +- (void) convertMouse:(int)rot x:(int*)x y:(int *)y +{ + int w = [self frame].size.width; + int h = [self frame].size.height; + int xx = *x, yy = *y; + int swap; + + if (ignore_rotation_p) { + // We need to rotate the coordinates to match the unrotated X11 window. + switch (orientation) { + case UIDeviceOrientationLandscapeRight: + swap = xx; xx = h-yy; yy = swap; + break; + case UIDeviceOrientationLandscapeLeft: + swap = xx; xx = yy; yy = w-swap; + break; + case UIDeviceOrientationPortraitUpsideDown: + xx = w-xx; yy = h-yy; + default: + break; + } + } + + double s = [self contentScaleFactor]; + *x = xx * s; + *y = yy * s; + +# if TARGET_IPHONE_SIMULATOR + NSLog (@"touch %4d, %-4d in %4d x %-4d %d %d\n", + *x, *y, (int)(w*s), (int)(h*s), + ignore_rotation_p, [self reshapeRotatedWindow]); +# endif +} + + +/* Single click exits saver. + */ +- (void) handleTap +{ + [self stopAndClose:NO]; +} + + +/* Double click sends Space KeyPress. + */ +- (void) handleDoubleTap +{ + if (!xsft->event_cb || !xwindow) return; + + 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]; +} + + +/* Drag with one finger down: send MotionNotify. + */ +- (void) handlePan:(UIGestureRecognizer *)sender +{ + if (!xsft->event_cb || !xwindow) return; + + XEvent xe; + memset (&xe, 0, sizeof(xe)); + + CGPoint p = [sender locationInView:self]; // this is in points, not pixels + int x = p.x; + int y = p.y; + [self convertMouse: rot_current_angle x:&x y:&y]; + 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]; +} + + +/* Hold one finger down: assume we're about to start dragging. + Treat the same as Pan. + */ +- (void) handleLongPress:(UIGestureRecognizer *)sender +{ + [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; + + XEvent xe; + memset (&xe, 0, sizeof(xe)); + + CGPoint p = [sender locationInView:self]; // this is in points, not pixels + int x = p.x; + int y = p.y; + [self convertMouse: rot_current_angle x:&x y:&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]; +} + + +/* We need this to respond to "shake" gestures + */ +- (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 +{ + if (screenLocked == locked) return; + screenLocked = locked; + if (locked) { + if ([self isAnimating]) + [self stopAnimation]; + } else { + if (! [self isAnimating]) + [self startAnimation]; + } +} + +#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 @@ -641,7 +2206,7 @@ static PrefsReader * get_prefsReader (Display *dpy) { XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0)); - if (!view) abort(); + if (!view) return 0; return [view prefsReader]; }