X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=OSX%2FXScreenSaverView.m;h=972ffb24806c83248196ad02f55571bc797d4cd1;hb=6afd6db0ae9396cd7ff897ade597cd5483f49b0e;hp=d773fc84d048ab6b75c4015befb677e5431867b5;hpb=4ade52359b6eba3621566dac79793a33aa4c915f;p=xscreensaver diff --git a/OSX/XScreenSaverView.m b/OSX/XScreenSaverView.m index d773fc84..972ffb24 100644 --- a/OSX/XScreenSaverView.m +++ b/OSX/XScreenSaverView.m @@ -1,13 +1,13 @@ -/* xscreensaver, Copyright (c) 2006-2013 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" @@ -16,8 +16,10 @@ */ #import +#import #import "XScreenSaverView.h" #import "XScreenSaverConfigSheet.h" +#import "Updater.h" #import "screenhackI.h" #import "xlockmoreI.h" #import "jwxyz-timers.h" @@ -220,6 +222,29 @@ 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 [] = { @@ -243,6 +268,21 @@ add_default_options (const XrmOptionDescRec *opts, # 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 }; @@ -320,6 +360,7 @@ double_time (void) 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]. @@ -341,6 +382,7 @@ double_time (void) [self setMultipleTouchEnabled:YES]; orientation = UIDeviceOrientationUnknown; [self didRotate:nil]; + [self initGestures]; # endif // USE_IPHONE setup_p = YES; @@ -385,10 +427,12 @@ double_time (void) - (void) initLayer { -# ifndef USE_IPHONE +# if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER) [self setLayer: [CALayer layer]]; + self.layer.delegate = self; + self.layer.opaque = YES; [self setWantsLayer: YES]; -# endif +# endif // !USE_IPHONE && BACKBUFFER_CALAYER } @@ -402,13 +446,21 @@ double_time (void) { NSAssert(![self isAnimating], @"still animating"); NSAssert(!xdata, @"xdata not yet freed"); - if (xdpy) - jwxyz_free_display (xdpy); + NSAssert(!xdpy, @"xdpy not yet freed"); # ifdef USE_BACKBUFFER if (backbuffer) CGContextRelease (backbuffer); -# endif + + if (colorspace) + CGColorSpaceRelease (colorspace); + +# ifdef BACKBUFFER_CGCONTEXT + if (window_ctx) + CGContextRelease (window_ctx); +# endif // BACKBUFFER_CGCONTEXT + +# endif // USE_BACKBUFFER [prefsReader release]; @@ -452,6 +504,10 @@ double_time (void) { 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 @@ -480,6 +536,8 @@ double_time (void) # ifdef USE_IPHONE [UIApplication sharedApplication].idleTimerDisabled = ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged); + [[UIApplication sharedApplication] + setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone]; # endif } @@ -502,6 +560,12 @@ double_time (void) 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; @@ -523,6 +587,8 @@ double_time (void) // # ifdef USE_IPHONE [UIApplication sharedApplication].idleTimerDisabled = NO; + [[UIApplication sharedApplication] + setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone]; # endif } @@ -568,9 +634,8 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) - (CGFloat) hackedContentScaleFactor { GLfloat s = [self contentScaleFactor]; - CGRect frame = [self bounds]; - if (frame.size.width >= 1024 || - frame.size.height >= 1024) + if (initial_bounds.width >= 1024 || + initial_bounds.height >= 1024) s = 1; return s; } @@ -677,12 +742,81 @@ double current_device_rotation (void) { # ifdef USE_IPHONE double s = [self hackedContentScaleFactor]; - int new_w = s * rot_current_size.width; - int new_h = s * rot_current_size.height; + CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size; + int new_w = s * rotsize.width; + int new_h = s * rotsize.height; # else int new_w = [self bounds].size.width; int new_h = [self bounds].size.height; # endif + + // Colorspaces and CGContexts only happen with non-GL hacks. + if (colorspace) + CGColorSpaceRelease (colorspace); +# ifdef BACKBUFFER_CGCONTEXT + if (window_ctx) + CGContextRelease (window_ctx); +# endif + + NSWindow *window = [self window]; + + if (window && xdpy) { + [self lockFocus]; + +# if defined(BACKBUFFER_CGCONTEXT) + // TODO: This was borrowed from jwxyz_window_resized, and should + // probably be refactored. + + // Figure out which screen the window is currently on. + CGDirectDisplayID cgdpy = 0; + + { +// int wx, wy; +// TODO: XTranslateCoordinates is returning (0,1200) on my system. +// Is this right? +// In any case, those weren't valid coordinates for CGGetDisplaysWithPoint. +// XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL); +// p.x = wx; +// p.y = wy; + + NSPoint p0 = {0, 0}; + p0 = [window convertBaseToScreen:p0]; + CGPoint p = {p0.x, p0.y}; + CGDisplayCount n; + CGGetDisplaysWithPoint (p, 1, &cgdpy, &n); + NSAssert (cgdpy, @"unable to find CGDisplay"); + } + + { + // Figure out this screen's colorspace, and use that for every CGImage. + // + CMProfileRef profile = 0; + + // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no + // documented replacement as of OS X 10.9. + // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html + CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile); + NSAssert (profile, @"unable to find colorspace profile"); + colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile); + NSAssert (colorspace, @"unable to find colorspace"); + } +# elif defined(BACKBUFFER_CALAYER) + // Was apparently faster until 10.9. + colorspace = CGColorSpaceCreateDeviceRGB (); +# endif // BACKBUFFER_CALAYER + +# ifdef BACKBUFFER_CGCONTEXT + window_ctx = [[window graphicsContext] graphicsPort]; + CGContextRetain (window_ctx); +# endif // BACKBUFFER_CGCONTEXT + + [self unlockFocus]; + } else { +# ifdef BACKBUFFER_CGCONTEXT + window_ctx = NULL; +# endif // BACKBUFFER_CGCONTEXT + colorspace = CGColorSpaceCreateDeviceRGB(); + } if (backbuffer && backbuffer_size.width == new_w && @@ -695,15 +829,16 @@ double current_device_rotation (void) backbuffer_size.width = new_w; backbuffer_size.height = new_h; - CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); backbuffer = CGBitmapContextCreate (NULL, backbuffer_size.width, backbuffer_size.height, 8, backbuffer_size.width * 4, - cs, - kCGImageAlphaPremultipliedLast); - CGColorSpaceRelease (cs); + colorspace, + // kCGImageAlphaPremultipliedLast + (kCGImageAlphaNoneSkipFirst | + kCGBitmapByteOrder32Host) + ); NSAssert (backbuffer, @"unable to allocate back buffer"); // Clear it. @@ -775,6 +910,13 @@ double current_device_rotation (void) 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]; } @@ -786,12 +928,6 @@ double current_device_rotation (void) initted_p = YES; resized_p = NO; NSAssert(!xdata, @"xdata already initialized"); - -# ifdef USE_IPHONE - /* Some X11 hacks (fluidballs) want to ignore all rotation events. */ - ignore_rotation_p = - get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation"); -# endif // USE_IPHONE # undef ya_rand_init @@ -825,11 +961,18 @@ double current_device_rotation (void) (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]; } @@ -854,24 +997,39 @@ double current_device_rotation (void) } - /* 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); @@ -910,6 +1068,8 @@ double current_device_rotation (void) # ifndef USE_IPHONE NSDisableScreenUpdates(); # endif + // NSAssert(xdata, @"no xdata when drawing"); + if (! xdata) abort(); unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata); if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata); # ifndef USE_IPHONE @@ -982,6 +1142,7 @@ double current_device_rotation (void) - (void) animateOneFrame { [self render_x11]; + jwxyz_flush_context(xdpy); } #else // USE_BACKBUFFER @@ -1004,31 +1165,117 @@ double current_device_rotation (void) # ifdef USE_IPHONE // Then compute the transformations for rotation. - - if (!ignore_rotation_p) { - // The rotation origin for layer.affineTransform is in the center already. - CGAffineTransform t = - CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI)); - - // Correct the aspect ratio. - CGRect frame = [self bounds]; - double s = [self hackedContentScaleFactor]; - t = CGAffineTransformScale(t, - backbuffer_size.width / (s * frame.size.width), - backbuffer_size.height / (s * frame.size.height)); - self.layer.affineTransform = t; - } + double hs = [self hackedContentScaleFactor]; + double s = [self contentScaleFactor]; + + // The rotation origin for layer.affineTransform is in the center already. + CGAffineTransform t = ignore_rotation_p ? + CGAffineTransformIdentity : + CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI)); + + CGFloat f = s / hs; + self.layer.affineTransform = CGAffineTransformScale(t, f, f); + + CGRect bounds; + bounds.origin.x = 0; + bounds.origin.y = 0; + bounds.size.width = backbuffer_size.width / s; + bounds.size.height = backbuffer_size.height / s; + self.layer.bounds = bounds; # endif // USE_IPHONE + +# if defined(BACKBUFFER_CALAYER) + [self.layer setNeedsDisplay]; +# elif defined(BACKBUFFER_CGCONTEXT) + size_t + w = CGBitmapContextGetWidth (backbuffer), + h = CGBitmapContextGetHeight (backbuffer); + + size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer); + CGDataProviderRef prov = CGDataProviderCreateWithData (NULL, + CGBitmapContextGetData(backbuffer), + bpl * h, + NULL); + + + CGImageRef img = CGImageCreate (w, h, + 8, 32, + CGBitmapContextGetBytesPerRow(backbuffer), + colorspace, + CGBitmapContextGetBitmapInfo(backbuffer), + prov, NULL, NO, + kCGRenderingIntentDefault); + + CGDataProviderRelease (prov); + + CGRect rect; + rect.origin.x = 0; + rect.origin.y = 0; + rect.size = backbuffer_size; + CGContextDrawImage (window_ctx, rect, img); + + CGImageRelease (img); - // Then copy that bitmap to the screen, by just stuffing it into - // the layer. The superclass drawRect method will handle the rest. + CGContextFlush (window_ctx); +# endif // BACKBUFFER_CGCONTEXT +} - CGImageRef img = CGBitmapContextCreateImage (backbuffer); - self.layer.contents = (id)img; - CGImageRelease (img); +# ifdef BACKBUFFER_CALAYER + +- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx +{ + // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4. + char *dest_data = (char *)CGBitmapContextGetData (ctx); + + // The CGContext here is normally upside-down on iOS. + if (dest_data && + CGBitmapContextGetBitmapInfo (ctx) == + (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host) +# ifdef USE_IPHONE + && CGContextGetCTM (ctx).d < 0 +# endif // USE_IPHONE + ) + { + size_t dest_height = CGBitmapContextGetHeight (ctx); + size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx); + size_t src_height = CGBitmapContextGetHeight (backbuffer); + size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer); + char *src_data = (char *)CGBitmapContextGetData (backbuffer); + + size_t height = src_height < dest_height ? src_height : dest_height; + + if (src_bpr == dest_bpr) { + // iPad 1: 4.0 ms, iPad 2: 6.7 ms + memcpy (dest_data, src_data, src_bpr * height); + } else { + // iPad 1: 4.6 ms, iPad 2: 7.2 ms + size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr; + while (height) { + memcpy (dest_data, src_data, bpr); + --height; + src_data += src_bpr; + dest_data += dest_bpr; + } + } + } else { + + // iPad 1: 9.6 ms, iPad 2: 12.1 ms + +# ifdef USE_IPHONE + CGContextScaleCTM (ctx, 1, -1); + CGFloat s = [self contentScaleFactor]; + CGFloat hs = [self hackedContentScaleFactor]; + CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s); +# endif // USE_IPHONE + + CGImageRef img = CGBitmapContextCreateImage (backbuffer); + CGContextDrawImage (ctx, self.layer.bounds, img); + CGImageRelease (img); + } } +# endif // BACKBUFFER_CALAYER -#endif // !USE_BACKBUFFER +#endif // USE_BACKBUFFER @@ -1061,6 +1308,40 @@ double current_device_rotation (void) 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 @@ -1084,16 +1365,19 @@ double current_device_rotation (void) NSWindow *sheet; # endif // !USE_IPHONE + NSData *xmld = [NSData dataWithContentsOfFile:path]; + NSString *xml = [[self class] decompressXML: xmld]; sheet = [[XScreenSaverConfigSheet alloc] - initWithXMLFile:path - options:xsft->options - controller:[prefsReader userDefaultsController] - defaults:[prefsReader defaultOptions]]; + initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding] + options:xsft->options + controller:[prefsReader userDefaultsController] + globalController:[prefsReader globalDefaultsController] + defaults:[prefsReader defaultOptions]]; // #### am I expected to retain this, or not? wtf. // I thought not, but if I don't do this, we (sometimes) crash. // #### Analyze says "potential leak of an object stored into sheet" - [sheet retain]; + // [sheet retain]; return sheet; } @@ -1106,26 +1390,62 @@ double current_device_rotation (void) /* 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)); @@ -1223,77 +1543,73 @@ double current_device_rotation (void) break; } - [self lockFocus]; - [self prepareContext]; - BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe); - [self unlockFocus]; - return result; + return [self sendEvent: &xe]; } - (void) mouseDown: (NSEvent *) e { - if (! [self doEvent:e type:ButtonPress]) + if (! [self convertEvent:e type:ButtonPress]) [super mouseDown:e]; } - (void) mouseUp: (NSEvent *) e { - if (! [self doEvent:e type:ButtonRelease]) + if (! [self convertEvent:e type:ButtonRelease]) [super mouseUp:e]; } - (void) otherMouseDown: (NSEvent *) e { - if (! [self doEvent:e type:ButtonPress]) + if (! [self convertEvent:e type:ButtonPress]) [super otherMouseDown:e]; } - (void) otherMouseUp: (NSEvent *) e { - if (! [self doEvent:e type:ButtonRelease]) + if (! [self convertEvent:e type:ButtonRelease]) [super otherMouseUp:e]; } - (void) mouseMoved: (NSEvent *) e { - if (! [self doEvent:e type:MotionNotify]) + if (! [self convertEvent:e type:MotionNotify]) [super mouseMoved:e]; } - (void) mouseDragged: (NSEvent *) e { - if (! [self doEvent:e type:MotionNotify]) + if (! [self convertEvent:e type:MotionNotify]) [super mouseDragged:e]; } - (void) otherMouseDragged: (NSEvent *) e { - if (! [self doEvent:e type:MotionNotify]) + if (! [self convertEvent:e type:MotionNotify]) [super otherMouseDragged:e]; } - (void) scrollWheel: (NSEvent *) e { - if (! [self doEvent:e type:ButtonPress]) + if (! [self convertEvent:e type:ButtonPress]) [super scrollWheel:e]; } - (void) keyDown: (NSEvent *) e { - if (! [self doEvent:e type:KeyPress]) + if (! [self convertEvent:e type:KeyPress]) [super keyDown:e]; } - (void) keyUp: (NSEvent *) e { - if (! [self doEvent:e type:KeyRelease]) + if (! [self convertEvent:e type:KeyRelease]) [super keyUp:e]; } - (void) flagsChanged: (NSEvent *) e { - if (! [self doEvent:e type:KeyPress]) + if (! [self convertEvent:e type:KeyPress]) [super flagsChanged:e]; } @@ -1354,7 +1670,7 @@ double current_device_rotation (void) Possibly XScreenSaverView should use Core Animation, and XScreenSaverGLView should override that. -*/ + */ - (void)didRotate:(NSNotification *)notification { UIDeviceOrientation current = [[UIDevice currentDevice] orientation]; @@ -1418,29 +1734,27 @@ double current_device_rotation (void) default: angle_to = 0; break; } - NSRect ff = [self bounds]; - switch (orientation) { case UIDeviceOrientationLandscapeRight: // from landscape case UIDeviceOrientationLandscapeLeft: - rot_from.width = ff.size.height; - rot_from.height = ff.size.width; + rot_from.width = initial_bounds.height; + rot_from.height = initial_bounds.width; break; default: // from portrait - rot_from.width = ff.size.width; - rot_from.height = ff.size.height; + rot_from.width = initial_bounds.width; + rot_from.height = initial_bounds.height; break; } switch (new_orientation) { case UIDeviceOrientationLandscapeRight: // to landscape case UIDeviceOrientationLandscapeLeft: - rot_to.width = ff.size.height; - rot_to.height = ff.size.width; + rot_to.width = initial_bounds.height; + rot_to.height = initial_bounds.width; break; default: // to portrait - rot_to.width = ff.size.width; - rot_to.height = ff.size.height; + rot_to.width = initial_bounds.width; + rot_to.height = initial_bounds.height; break; } @@ -1453,176 +1767,201 @@ double current_device_rotation (void) } -/* I believe we can't use UIGestureRecognizer for tracking touches - because UIPanGestureRecognizer doesn't give us enough detail in its - callbacks. +/* We distinguish between taps and drags. - Currently we don't handle multi-touches (just the first touch) but - I'm leaving this comment here for future reference: + - Drags/pans (down, motion, up) are sent to the saver to handle. + - Single-taps exit the saver. + - Double-taps are sent to the saver as a "Space" keypress. + - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys. - In the simulator, multi-touch sequences look like this: + This means a saver cannot respond to a single-tap. Only a few try to. + */ - touchesBegan [touchA, touchB] - touchesEnd [touchA, touchB] +- (void)initGestures +{ + UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleDoubleTap)]; + dtap.numberOfTapsRequired = 2; + dtap.numberOfTouchesRequired = 1; + + UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleTap)]; + stap.numberOfTapsRequired = 1; + stap.numberOfTouchesRequired = 1; + + UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] + initWithTarget:self + action:@selector(handlePan:)]; + pan.maximumNumberOfTouches = 1; + pan.minimumNumberOfTouches = 1; + + // I couldn't get Swipe to work, but using a second Pan recognizer works. + UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc] + initWithTarget:self + action:@selector(handlePan2:)]; + pan2.maximumNumberOfTouches = 2; + pan2.minimumNumberOfTouches = 2; + + // Also handle long-touch, and treat that the same as Pan. + // Without this, panning doesn't start until there's motion, so the trick + // of holding down your finger to freeze the scene doesn't work. + // + UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleLongPress:)]; + hold.numberOfTapsRequired = 0; + hold.numberOfTouchesRequired = 1; + hold.minimumPressDuration = 0.25; /* 1/4th second */ - But on real devices, sometimes you get that, but sometimes you get: + [stap requireGestureRecognizerToFail: dtap]; + [stap requireGestureRecognizerToFail: hold]; + [dtap requireGestureRecognizerToFail: hold]; + [pan requireGestureRecognizerToFail: hold]; - touchesBegan [touchA, touchB] - touchesEnd [touchB] - touchesEnd [touchA] + [self addGestureRecognizer: dtap]; + [self addGestureRecognizer: stap]; + [self addGestureRecognizer: pan]; + [self addGestureRecognizer: pan2]; + [self addGestureRecognizer: hold]; - Or even + [dtap release]; + [stap release]; + [pan release]; + [pan2 release]; + [hold release]; +} - touchesBegan [touchA] - touchesBegan [touchB] - touchesEnd [touchA] - touchesEnd [touchB] - So the only way to properly detect a "pinch" gesture is to remember - the start-point of each touch as it comes in; and the end-point of - each touch as those come in; and only process the gesture once the - number of touchEnds matches the number of touchBegins. - */ - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h { - CGRect frame = [self bounds]; // Correct aspect ratio and scale. + // This is a no-op unless contentScaleFactor != hackedContentScaleFactor. + // Currently, this is the iPad Retina only. + CGRect frame = [self bounds]; // Scale. double s = [self hackedContentScaleFactor]; *x *= (backbuffer_size.width / frame.size.width) / s; *y *= (backbuffer_size.height / frame.size.height) / s; } -#if 0 // AudioToolbox/AudioToolbox.h -- (void) beep +/* Single click exits saver. + */ +- (void) handleTap { - // There's no way to play a standard system alert sound! - // We'd have to include our own WAV for that. Eh, fuck it. - AudioServicesPlaySystemSound (kSystemSoundID_Vibrate); -# if TARGET_IPHONE_SIMULATOR - NSLog(@"BEEP"); // The sim doesn't vibrate. -# endif + [self stopAndClose:NO]; } -#endif -/* We distinguish between taps and drags. - - Drags (down, motion, up) are sent to the saver to handle. - - Single-taps exit the saver. - This means a saver cannot respond to a single-tap. Only a few try to. +/* Double click sends Space KeyPress. */ - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +- (void) handleDoubleTap { - // If they are trying to pinch, just do nothing. - if ([[event allTouches] count] > 1) - return; + if (!xsft->event_cb || !xwindow) return; - tap_time = 0; - - if (xsft->event_cb && xwindow) { - double s = [self hackedContentScaleFactor]; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - // #### 'frame' here or 'bounds'? - int w = s * [self frame].size.width; - int h = s * [self frame].size.height; - for (UITouch *touch in touches) { - CGPoint p = [touch locationInView:self]; - xe.xany.type = ButtonPress; - xe.xbutton.button = i + 1; - xe.xbutton.button = i + 1; - xe.xbutton.x = s * p.x; - xe.xbutton.y = s * p.y; - [self rotateMouse: rot_current_angle - x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; - jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); - - // Ignore return code: don't care whether the hack handled it. - xsft->event_cb (xdpy, xwindow, xdata, &xe); - - // Remember when/where this was, to determine tap versus drag or hold. - tap_time = double_time(); - tap_point = p; - - i++; - break; // No pinches: only look at the first touch. - } - } + XEvent xe; + memset (&xe, 0, sizeof(xe)); + xe.xkey.keycode = ' '; + xe.xany.type = KeyPress; + BOOL ok1 = [self sendEvent: &xe]; + xe.xany.type = KeyRelease; + BOOL ok2 = [self sendEvent: &xe]; + if (!(ok1 || ok2)) + [self beep]; } -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +/* Drag with one finger down: send MotionNotify. + */ +- (void) handlePan:(UIGestureRecognizer *)sender { - // If they are trying to pinch, just do nothing. - if ([[event allTouches] count] > 1) - return; + if (!xsft->event_cb || !xwindow) return; - if (xsft->event_cb && xwindow) { - double s = [self hackedContentScaleFactor]; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - // #### 'frame' here or 'bounds'? - int w = s * [self frame].size.width; - int h = s * [self frame].size.height; - for (UITouch *touch in touches) { - CGPoint p = [touch locationInView:self]; - - // If the ButtonRelease came less than half a second after ButtonPress, - // and didn't move far, then this was a tap, not a drag or a hold. - // Interpret it as "exit". - // - double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) + - ((p.y - tap_point.y) * (p.y - tap_point.y))); - if (tap_time + 0.5 >= double_time() && dist < 20) { - [self stopAndClose:NO]; - return; - } + double s = [self hackedContentScaleFactor]; + XEvent xe; + memset (&xe, 0, sizeof(xe)); - xe.xany.type = ButtonRelease; - xe.xbutton.button = i + 1; - xe.xbutton.x = s * p.x; - xe.xbutton.y = s * p.y; - [self rotateMouse: rot_current_angle - x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; - jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); - xsft->event_cb (xdpy, xwindow, xdata, &xe); - i++; - break; // No pinches: only look at the first touch. - } + CGPoint p = [sender locationInView:self]; + int x = s * p.x; + int y = s * p.y; + int w = s * [self frame].size.width; // #### 'frame' here or 'bounds'? + int h = s * [self frame].size.height; + [self rotateMouse: rot_current_angle x:&x y:&y w:w h:h]; + jwxyz_mouse_moved (xdpy, xwindow, x, y); + + switch (sender.state) { + case UIGestureRecognizerStateBegan: + xe.xany.type = ButtonPress; + xe.xbutton.button = 1; + xe.xbutton.x = x; + xe.xbutton.y = y; + break; + + case UIGestureRecognizerStateEnded: + xe.xany.type = ButtonRelease; + xe.xbutton.button = 1; + xe.xbutton.x = x; + xe.xbutton.y = y; + break; + + case UIGestureRecognizerStateChanged: + xe.xany.type = MotionNotify; + xe.xmotion.x = x; + xe.xmotion.y = y; + break; + + default: + break; } + + BOOL ok = [self sendEvent: &xe]; + if (!ok && xe.xany.type == ButtonRelease) + [self beep]; } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +/* Hold one finger down: assume we're about to start dragging. + Treat the same as Pan. + */ +- (void) handleLongPress:(UIGestureRecognizer *)sender { - // If they are trying to pinch, just do nothing. - if ([[event allTouches] count] > 1) + [self handlePan:sender]; +} + + + +/* Drag with 2 fingers down: send arrow keys. + */ +- (void) handlePan2:(UIPanGestureRecognizer *)sender +{ + if (!xsft->event_cb || !xwindow) return; + + if (sender.state != UIGestureRecognizerStateEnded) return; - if (xsft->event_cb && xwindow) { - double s = [self hackedContentScaleFactor]; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - // #### 'frame' here or 'bounds'? - int w = s * [self frame].size.width; - int h = s * [self frame].size.height; - for (UITouch *touch in touches) { - CGPoint p = [touch locationInView:self]; - xe.xany.type = MotionNotify; - xe.xmotion.x = s * p.x; - xe.xmotion.y = s * p.y; - [self rotateMouse: rot_current_angle - x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; - jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y); - xsft->event_cb (xdpy, xwindow, xdata, &xe); - i++; - break; // No pinches: only look at the first touch. - } - } + double s = [self hackedContentScaleFactor]; + XEvent xe; + memset (&xe, 0, sizeof(xe)); + + CGPoint p = [sender translationInView:self]; + int x = s * p.x; + int y = s * p.y; + int w = s * [self frame].size.width; // #### 'frame' here or 'bounds'? + int h = s * [self frame].size.height; + [self rotateMouse: rot_current_angle x:&x y:&y w:w h:h]; + // jwxyz_mouse_moved (xdpy, xwindow, x, y); + + if (abs(x) > abs(y)) + xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left); + else + xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up); + + BOOL ok1 = [self sendEvent: &xe]; + xe.xany.type = KeyRelease; + BOOL ok2 = [self sendEvent: &xe]; + if (!(ok1 || ok2)) + [self beep]; } @@ -1663,10 +2002,76 @@ double current_device_rotation (void) } } - #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...