X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=OSX%2FXScreenSaverView.m;h=d773fc84d048ab6b75c4015befb677e5431867b5;hb=4ade52359b6eba3621566dac79793a33aa4c915f;hp=8c6b5ea4762321c225db7e2f49dc23ef8058d323;hpb=f8cf5ac7b2f53510f80a0eaf286a25298be17bfe;p=xscreensaver diff --git a/OSX/XScreenSaverView.m b/OSX/XScreenSaverView.m index 8c6b5ea4..d773fc84 100644 --- a/OSX/XScreenSaverView.m +++ b/OSX/XScreenSaverView.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski +/* 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 @@ -22,6 +22,7 @@ #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. */ @@ -44,6 +45,8 @@ int mono_p = 0; # ifdef USE_IPHONE +extern NSDictionary *make_function_table_dict(void); // ios-function-table.m + /* Stub definition of the superclass, for iPhone. */ @implementation ScreenSaverView @@ -91,6 +94,10 @@ int mono_p = 0; +@interface XScreenSaverView (Private) +- (void) stopAndClose:(Bool)relaunch; +@end + @implementation XScreenSaverView // Given a lower-cased saver name, returns the function table for it. @@ -109,19 +116,35 @@ 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]; - NSString *table_name = [[name lowercaseString] - stringByAppendingString: - @"_xscreensaver_function_table"]; + name = [[name lowercaseString] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + +# ifndef USE_IPHONE + // CFBundleGetDataPointerForName doesn't work in "Archive" builds. + // I'm guessing that symbol-stripping is mandatory. Fuck. + NSString *table_name = [name stringByAppendingString: + @"_xscreensaver_function_table"]; void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name); CFRelease (cfb); if (! addr) NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path); +# else // USE_IPHONE + // Remember: any time you add a new saver to the iOS app, + // manually run "make ios-function-table.m"! + if (! function_tables) + function_tables = [make_function_table_dict() retain]; + NSValue *v = [function_tables objectForKey: name]; + void *addr = v ? [v pointerValue] : 0; +# endif // USE_IPHONE + return (struct xscreensaver_function_table *) addr; } @@ -146,7 +169,7 @@ int mono_p = 0; strcat (npath, opath); if (putenv (npath)) { perror ("putenv"); - abort(); + NSAssert1 (0, @"putenv \"%s\" failed", npath); } /* Don't free (npath) -- MacOS's putenv() does not copy it. */ @@ -167,7 +190,7 @@ int mono_p = 0; strcat (env, s); if (putenv (env)) { perror ("putenv"); - abort(); + NSAssert1 (0, @"putenv \"%s\" failed", env); } /* Don't free (env) -- MacOS's putenv() does not copy it. */ } @@ -210,10 +233,14 @@ add_default_options (const XrmOptionDescRec *opts, # endif // ".textLiteral: ", // ".textFile: ", - ".textURL: http://twitter.com/statuses/public_timeline.atom", + ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss", // ".textProgram: ", ".grabDesktopImages: yes", +# ifndef USE_IPHONE ".chooseRandomImages: no", +# else + ".chooseRandomImages: yes", +# endif ".imageDirectory: ~/Pictures", ".relaunchDelay: 2", 0 @@ -325,7 +352,7 @@ double_time (void) "org.jwz.xscreensaver...plist" */ NSString *name = [NSString stringWithCString:xsft->progclass - encoding:NSUTF8StringEncoding]; + encoding:NSISOLatin1StringEncoding]; name = [@"org.jwz.xscreensaver." stringByAppendingString:name]; [self setResourcesEnv:name]; @@ -343,9 +370,12 @@ double_time (void) next_frame_time = 0; -# ifdef USE_IPHONE +# ifdef USE_BACKBUFFER [self createBackbuffer]; + [self initLayer]; +# endif +# ifdef USE_IPHONE // So we can tell when we're docked. [UIDevice currentDevice].batteryMonitoringEnabled = YES; # endif // USE_IPHONE @@ -353,6 +383,15 @@ double_time (void) return self; } +- (void) initLayer +{ +# ifndef USE_IPHONE + [self setLayer: [CALayer layer]]; + [self setWantsLayer: YES]; +# endif +} + + - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p { return [self initWithFrame:frame saverName:0 isPreview:p]; @@ -366,7 +405,7 @@ double_time (void) if (xdpy) jwxyz_free_display (xdpy); -# ifdef USE_IPHONE +# ifdef USE_BACKBUFFER if (backbuffer) CGContextRelease (backbuffer); # endif @@ -375,7 +414,6 @@ double_time (void) // xsft // fpst - // orientation_timer [super dealloc]; } @@ -400,10 +438,12 @@ double_time (void) */ - (void) allSystemsGo: (NSTimer *) timer { - NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; - [prefs setBool:YES forKey:@"wasRunning"]; NSAssert (timer == crash_timer, @"crash timer screwed up"); crash_timer = 0; + + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + [prefs setBool:YES forKey:@"wasRunning"]; + [prefs synchronize]; } #endif // USE_IPHONE @@ -421,11 +461,17 @@ double_time (void) # 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, @@ -467,6 +513,7 @@ double_time (void) crash_timer = 0; NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; [prefs removeObjectForKey:@"wasRunning"]; + [prefs synchronize]; # endif // USE_IPHONE [super stopAnimation]; @@ -500,48 +547,35 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) fps_draw (fpst); } + #ifdef USE_IPHONE -/* Create a bitmap context into which we render everything. +/* On iPhones with Retina displays, we can draw the savers in "real" + pixels, and that works great. The 320x480 "point" screen is really + a 640x960 *pixel* screen. However, Retina iPads have 768x1024 + point screens which are 1536x2048 pixels, and apparently that's + enough pixels that copying those bits to the screen is slow. Like, + drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in + real pixels. This will probably make the savers look better + anyway, since that's a higher resolution than most desktop monitors + have even today. (This is only true for X11 programs, not GL + programs. Those are fine at full rez.) + + This method is overridden in XScreenSaverGLView, since this kludge + isn't necessary for GL programs, being resolution independent by + nature. */ -- (void) createBackbuffer +- (CGFloat) hackedContentScaleFactor { - CGContextRef ob = backbuffer; - NSSize osize = backbuffer_size; - - CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); - double s = self.contentScaleFactor; - backbuffer_size.width = (int) (s * rot_current_size.width); - backbuffer_size.height = (int) (s * rot_current_size.height); - backbuffer = CGBitmapContextCreate (NULL, - backbuffer_size.width, - backbuffer_size.height, - 8, - backbuffer_size.width * 4, - cs, - kCGImageAlphaPremultipliedLast); - NSAssert (backbuffer, @"unable to allocate back buffer"); - CGColorSpaceRelease (cs); - - // Clear it. - CGContextSetGrayFillColor (backbuffer, 0, 1); - CGRect r = CGRectZero; - r.size = backbuffer_size; - CGContextFillRect (backbuffer, r); - - if (ob) { - // Restore old bits, as much as possible, to the X11 upper left origin. - NSRect rect; - rect.origin.x = 0; - rect.origin.y = (backbuffer_size.height - osize.height); - rect.size = osize; - CGImageRef img = CGBitmapContextCreateImage (ob); - CGContextDrawImage (backbuffer, rect, img); - CGImageRelease (img); - CGContextRelease (ob); - } + GLfloat s = [self contentScaleFactor]; + CGRect frame = [self bounds]; + if (frame.size.width >= 1024 || + frame.size.height >= 1024) + s = 1; + return s; } + static GLfloat _global_rot_current_angle_kludge; double current_device_rotation (void) @@ -596,19 +630,136 @@ double current_device_rotation (void) # undef CLAMP180 - double s = self.contentScaleFactor; - if (((int) backbuffer_size.width != (int) (s * rot_current_size.width) || - (int) backbuffer_size.height != (int) (s * rot_current_size.height)) -/* && rotation_ratio == -1*/) - [self setFrame:[self frame]]; + double s = [self hackedContentScaleFactor]; + if (!ignore_rotation_p && + /* rotation_ratio && */ + ((int) backbuffer_size.width != (int) (s * rot_current_size.width) || + (int) backbuffer_size.height != (int) (s * rot_current_size.height))) + [self resize_x11]; +} + + +- (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i +{ + if (i == 0) exit (-1); // Cancel + [self stopAndClose:NO]; // Keep going +} + +- (void) handleException: (NSException *)e +{ + NSLog (@"Caught exception: %@", e); + [[[UIAlertView alloc] initWithTitle: + [NSString stringWithFormat: @"%s crashed!", + xsft->progclass] + message: + [NSString stringWithFormat: + @"The error message was:" + "\n\n%@\n\n" + "If it keeps crashing, try " + "resetting its options.", + e] + delegate: self + cancelButtonTitle: @"Exit" + otherButtonTitles: @"Keep going", nil] + show]; + [self stopAnimation]; } #endif // USE_IPHONE +#ifdef USE_BACKBUFFER + +/* Create a bitmap context into which we render everything. + If the desired size has changed, re-created it. + */ +- (void) createBackbuffer +{ +# ifdef USE_IPHONE + double s = [self hackedContentScaleFactor]; + int new_w = s * rot_current_size.width; + int new_h = s * rot_current_size.height; +# else + int new_w = [self bounds].size.width; + int new_h = [self bounds].size.height; +# endif + + if (backbuffer && + backbuffer_size.width == new_w && + backbuffer_size.height == new_h) + return; + + CGSize osize = backbuffer_size; + CGContextRef ob = backbuffer; + + backbuffer_size.width = new_w; + backbuffer_size.height = new_h; + + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + backbuffer = CGBitmapContextCreate (NULL, + backbuffer_size.width, + backbuffer_size.height, + 8, + backbuffer_size.width * 4, + cs, + kCGImageAlphaPremultipliedLast); + CGColorSpaceRelease (cs); + NSAssert (backbuffer, @"unable to allocate back buffer"); + + // Clear it. + CGRect r; + r.origin.x = r.origin.y = 0; + r.size = backbuffer_size; + CGContextSetGrayFillColor (backbuffer, 0, 1); + CGContextFillRect (backbuffer, r); + + if (ob) { + // Restore old bits, as much as possible, to the X11 upper left origin. + CGRect rect; + rect.origin.x = 0; + rect.origin.y = (backbuffer_size.height - osize.height); + rect.size = osize; + CGImageRef img = CGBitmapContextCreateImage (ob); + CGContextDrawImage (backbuffer, rect, img); + CGImageRelease (img); + CGContextRelease (ob); + } +} + +#endif // USE_BACKBUFFER + + +/* Inform X11 that the size of our window has changed. + */ +- (void) resize_x11 +{ + if (!xwindow) return; // early + +# ifdef USE_BACKBUFFER + [self createBackbuffer]; + jwxyz_window_resized (xdpy, xwindow, + 0, 0, + backbuffer_size.width, backbuffer_size.height, + backbuffer); +# else // !USE_BACKBUFFER + NSRect r = [self frame]; // ignoring rotation is closer + r.size = [self bounds].size; // to what XGetGeometry expects. + jwxyz_window_resized (xdpy, xwindow, + r.origin.x, r.origin.y, + r.size.width, r.size.height, + 0); +# endif // !USE_BACKBUFFER + + // Next time render_x11 is called, run the saver's reshape_cb. + resized_p = YES; +} + + - (void) render_x11 { # ifdef USE_IPHONE + @try { + if (orientation == UIDeviceOrientationUnknown) [self didRotate:nil]; [self hackRotation]; @@ -617,26 +768,14 @@ double current_device_rotation (void) if (!initted_p) { if (! xdpy) { -# ifdef USE_IPHONE +# 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 - jwxyz_window_resized (xdpy, xwindow, - 0, 0, - backbuffer_size.width, backbuffer_size.height, - backbuffer); -# else - NSRect r = [self frame]; - jwxyz_window_resized (xdpy, xwindow, - r.origin.x, r.origin.y, - r.size.width, r.size.height, - 0); -# endif + [self resize_x11]; } if (!setup_p) { @@ -648,6 +787,13 @@ double current_device_rotation (void) 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 ya_rand_init (0); @@ -739,20 +885,19 @@ double current_device_rotation (void) // Xlib drawing takes place under the animation timer. [self resizeContext]; NSRect r; -# ifndef USE_IPHONE - r = [self frame]; -# else // USE_IPHONE +# ifndef USE_BACKBUFFER + r = [self bounds]; +# else // USE_BACKBUFFER r.origin.x = 0; r.origin.y = 0; r.size.width = backbuffer_size.width; r.size.height = backbuffer_size.height; -# endif // USE_IPHONE +# endif // USE_BACKBUFFER xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height); resized_p = NO; } - // Run any XtAppAddInput callbacks now. // (Note that XtAppAddTimeOut callbacks have already been run by // the Cocoa event loop.) @@ -809,14 +954,19 @@ double current_device_rotation (void) } } # endif // DO_GC_HACKERY + +# ifdef USE_IPHONE + } + @catch (NSException *e) { + [self handleException: e]; + } +# endif // USE_IPHONE } -/* On MacOS: drawRect does nothing, and animateOneFrame renders. - On iOS GL: drawRect does nothing, and animateOneFrame renders. - On iOS X11: drawRect renders, and animateOneFrame marks the view dirty. +/* drawRect always does nothing, and animateOneFrame renders bits to the + screen. This is (now) true of both X11 and GL on both MacOS and iOS. */ -#ifndef USE_IPHONE - (void)drawRect:(NSRect)rect { @@ -826,78 +976,59 @@ double current_device_rotation (void) [super drawRect:rect]; // early: black. } + +#ifndef USE_BACKBUFFER + - (void) animateOneFrame { [self render_x11]; } -#else // USE_IPHONE +#else // USE_BACKBUFFER -- (void)drawRect:(NSRect)rect +- (void) animateOneFrame { // Render X11 into the backing store bitmap... NSAssert (backbuffer, @"no back buffer"); - UIGraphicsPushContext (backbuffer); - [self render_x11]; - UIGraphicsPopContext(); - - // Then copy that bitmap to the screen. - - CGContextRef cgc = UIGraphicsGetCurrentContext(); - - // Mask it to only update the parts that are exposed. -// CGContextClipToRect (cgc, rect); - double s = self.contentScaleFactor; - CGRect frame = [self frame]; - - NSRect target; - target.size.width = backbuffer_size.width; - target.size.height = backbuffer_size.height; - target.origin.x = (s * frame.size.width - target.size.width) / 2; - target.origin.y = (s * frame.size.height - target.size.height) / 2; - - target.origin.x /= s; - target.origin.y /= s; - target.size.width /= s; - target.size.height /= s; - - CGAffineTransform t = CGAffineTransformIdentity; +# ifdef USE_IPHONE + UIGraphicsPushContext (backbuffer); +# endif - // Rotate around center - float cx = frame.size.width / 2; - float cy = frame.size.height / 2; - t = CGAffineTransformTranslate (t, cx, cy); - t = CGAffineTransformRotate (t, -rot_current_angle / (180.0 / M_PI)); - t = CGAffineTransformTranslate (t, -cx, -cy); + [self render_x11]; - // Flip Y axis - t = CGAffineTransformConcat (t, - CGAffineTransformMake ( 1, 0, 0, - -1, 0, frame.size.height)); +# ifdef USE_IPHONE + UIGraphicsPopContext(); +# endif - // Clear background (visible in corners of screen during rotation) - if (rotation_ratio != -1) { - CGContextSetGrayFillColor (cgc, 0, 1); - CGContextFillRect (cgc, frame); +# 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; } +# endif // USE_IPHONE - CGContextConcatCTM (cgc, t); + // Then copy that bitmap to the screen, by just stuffing it into + // the layer. The superclass drawRect method will handle the rest. - // Copy the backbuffer to the screen. - // Note that CGContextDrawImage measures in "points", not "pixels". CGImageRef img = CGBitmapContextCreateImage (backbuffer); - CGContextDrawImage (cgc, target, img); + self.layer.contents = (id)img; CGImageRelease (img); } -- (void) animateOneFrame -{ - [self setNeedsDisplay]; -} - -#endif // !USE_IPHONE +#endif // !USE_BACKBUFFER @@ -905,26 +1036,8 @@ double current_device_rotation (void) { [super setFrame:newRect]; -# ifdef USE_IPHONE - [self createBackbuffer]; -# endif - - resized_p = YES; // The reshape_cb runs in render_x11 - if (xwindow) { // inform Xlib that the window has changed now. -# ifdef USE_IPHONE - NSAssert (backbuffer, @"no back buffer"); - // The backbuffer is the rotated size, and so is the xwindow. - jwxyz_window_resized (xdpy, xwindow, - 0, 0, - backbuffer_size.width, backbuffer_size.height, - backbuffer); -# else - jwxyz_window_resized (xdpy, xwindow, - newRect.origin.x, newRect.origin.y, - newRect.size.width, newRect.size.height, - 0); -# endif - } + if (xwindow) // inform Xlib that the window has changed now. + [self resize_x11]; } @@ -932,13 +1045,8 @@ double current_device_rotation (void) - (void) setFrameSize:(NSSize) newSize { [super setFrameSize:newSize]; - resized_p = YES; if (xwindow) - jwxyz_window_resized (xdpy, xwindow, - [self frame].origin.x, - [self frame].origin.y, - newSize.width, newSize.height, - 0); // backbuffer only on iPhone + [self resize_x11]; } # endif // !USE_IPHONE @@ -961,7 +1069,7 @@ double current_device_rotation (void) { 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) { @@ -984,6 +1092,7 @@ double current_device_rotation (void) // #### am I expected to retain this, or not? wtf. // I thought not, but if I don't do this, we (sometimes) crash. + // #### Analyze says "potential leak of an object stored into sheet" [sheet retain]; return sheet; @@ -1032,12 +1141,12 @@ double current_device_rotation (void) NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow] toView:self]; # ifdef USE_IPHONE - double s = self.contentScaleFactor; + double s = [self hackedContentScaleFactor]; # else int s = 1; # endif int x = s * p.x; - int y = s * ([self frame].size.height - p.y); + int y = s * ([self bounds].size.height - p.y); xe.xany.type = type; switch (type) { @@ -1103,12 +1212,15 @@ double current_device_rotation (void) } } + 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]; @@ -1188,6 +1300,44 @@ double current_device_rotation (void) #else // USE_IPHONE +- (void) stopAndClose:(Bool)relaunch_p +{ + if ([self isAnimating]) + [self stopAnimation]; + + /* Need to make the SaverListController be the firstResponder again + so that it can continue to receive its own shake events. I + suppose that this abstraction-breakage means that I'm adding + XScreenSaverView to the UINavigationController wrong... + */ + UIViewController *v = [[self window] rootViewController]; + if ([v isKindOfClass: [UINavigationController class]]) { + UINavigationController *n = (UINavigationController *) v; + [[n topViewController] becomeFirstResponder]; + } + + UIView *fader = [self superview]; // the "backgroundView" view is our parent + + if (relaunch_p) { // Fake a shake on the SaverListController. + // Why is [self window] sometimes null here? + UIWindow *w = [[UIApplication sharedApplication] keyWindow]; + UIViewController *v = [w rootViewController]; + if ([v isKindOfClass: [UINavigationController class]]) { + UINavigationController *n = (UINavigationController *) v; + [[n topViewController] motionEnded: UIEventSubtypeMotionShake + withEvent: nil]; + } + } else { // Not launching another, animate our return to the list. + [UIView animateWithDuration: 0.5 + animations:^{ fader.alpha = 0.0; } + completion:^(BOOL finished) { + [fader removeFromSuperview]; + fader.alpha = 1.0; + }]; + } +} + + /* Called after the device's orientation has changed. Note: we could include a subclass of UIViewController which @@ -1245,29 +1395,34 @@ double current_device_rotation (void) if (rotation_ratio >= 0) return; // in the midst of rotation animation if (orientation == current) return; // no change + // When transitioning to FaceUp or FaceDown, pretend there was no change. + if (current == UIDeviceOrientationFaceUp || + current == UIDeviceOrientationFaceDown) + return; + new_orientation = current; // current animation target rotation_ratio = 0; // start animating rot_start_time = double_time(); switch (orientation) { - case UIInterfaceOrientationLandscapeRight: angle_from = 90; break; - case UIInterfaceOrientationLandscapeLeft: angle_from = 270; break; - case UIInterfaceOrientationPortraitUpsideDown: angle_from = 180; break; - default: angle_from = 0; break; + case UIDeviceOrientationLandscapeLeft: angle_from = 90; break; + case UIDeviceOrientationLandscapeRight: angle_from = 270; break; + case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break; + default: angle_from = 0; break; } switch (new_orientation) { - case UIInterfaceOrientationLandscapeRight: angle_to = 90; break; - case UIInterfaceOrientationLandscapeLeft: angle_to = 270; break; - case UIInterfaceOrientationPortraitUpsideDown: angle_to = 180; break; - default: angle_to = 0; break; + case UIDeviceOrientationLandscapeLeft: angle_to = 90; break; + case UIDeviceOrientationLandscapeRight: angle_to = 270; break; + case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break; + default: angle_to = 0; break; } - NSRect ff = [self frame]; + NSRect ff = [self bounds]; switch (orientation) { - case UIInterfaceOrientationLandscapeLeft: // from landscape - case UIInterfaceOrientationLandscapeRight: + case UIDeviceOrientationLandscapeRight: // from landscape + case UIDeviceOrientationLandscapeLeft: rot_from.width = ff.size.height; rot_from.height = ff.size.width; break; @@ -1278,8 +1433,8 @@ double current_device_rotation (void) } switch (new_orientation) { - case UIInterfaceOrientationLandscapeLeft: // to landscape - case UIInterfaceOrientationLandscapeRight: + case UIDeviceOrientationLandscapeRight: // to landscape + case UIDeviceOrientationLandscapeLeft: rot_to.width = ff.size.height; rot_to.height = ff.size.width; break; @@ -1298,7 +1453,14 @@ double current_device_rotation (void) } -/* In the simulator, multi-touch sequences look like this: +/* I believe we can't use UIGestureRecognizer for tracking touches + because UIPanGestureRecognizer doesn't give us enough detail in its + callbacks. + + Currently we don't handle multi-touches (just the first touch) but + I'm leaving this comment here for future reference: + + In the simulator, multi-touch sequences look like this: touchesBegan [touchA, touchB] touchesEnd [touchA, touchB] @@ -1322,23 +1484,48 @@ double current_device_rotation (void) number of touchEnds matches the number of touchBegins. */ -static void -rotate_mouse (int *x, int *y, int w, int h, int rot) +- (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h { - int ox = *x, oy = *y; - if (rot > 45 && rot < 135) { *x = oy; *y = w-ox; } - else if (rot < -45 && rot > -135) { *x = h-oy; *y = ox; } - else if (rot > 135 || rot < -135) { *x = w-ox; *y = h-oy; } + CGRect frame = [self bounds]; // Correct aspect ratio and scale. + double s = [self hackedContentScaleFactor]; + *x *= (backbuffer_size.width / frame.size.width) / s; + *y *= (backbuffer_size.height / frame.size.height) / s; } +#if 0 // AudioToolbox/AudioToolbox.h +- (void) beep +{ + // There's no way to play a standard system alert sound! + // We'd have to include our own WAV for that. Eh, fuck it. + AudioServicesPlaySystemSound (kSystemSoundID_Vibrate); +# if TARGET_IPHONE_SIMULATOR + NSLog(@"BEEP"); // The sim doesn't vibrate. +# endif +} +#endif + + +/* We distinguish between taps and drags. + - Drags (down, motion, up) are sent to the saver to handle. + - Single-taps exit the saver. + This means a saver cannot respond to a single-tap. Only a few try to. + */ + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + // If they are trying to pinch, just do nothing. + if ([[event allTouches] count] > 1) + return; + + tap_time = 0; + if (xsft->event_cb && xwindow) { - double s = self.contentScaleFactor; + double s = [self hackedContentScaleFactor]; XEvent xe; memset (&xe, 0, sizeof(xe)); int i = 0; + // #### 'frame' here or 'bounds'? int w = s * [self frame].size.width; int h = s * [self frame].size.height; for (UITouch *touch in touches) { @@ -1348,10 +1535,19 @@ rotate_mouse (int *x, int *y, int w, int h, int rot) xe.xbutton.button = i + 1; xe.xbutton.x = s * p.x; xe.xbutton.y = s * p.y; - rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); + [self rotateMouse: rot_current_angle + x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); + + // Ignore return code: don't care whether the hack handled it. xsft->event_cb (xdpy, xwindow, xdata, &xe); + + // Remember when/where this was, to determine tap versus drag or hold. + tap_time = double_time(); + tap_point = p; + i++; + break; // No pinches: only look at the first touch. } } } @@ -1359,35 +1555,42 @@ rotate_mouse (int *x, int *y, int w, int h, int rot) - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - - // Double-tap means "exit" and return to selection menu. - // - for (UITouch *touch in touches) { - if ([touch tapCount] >= 2) { - if ([self isAnimating]) - [self stopAnimation]; - [self removeFromSuperview]; - return; - } - } + // If they are trying to pinch, just do nothing. + if ([[event allTouches] count] > 1) + return; if (xsft->event_cb && xwindow) { - double s = self.contentScaleFactor; + double s = [self hackedContentScaleFactor]; XEvent xe; memset (&xe, 0, sizeof(xe)); int i = 0; + // #### 'frame' here or 'bounds'? int w = s * [self frame].size.width; int h = s * [self frame].size.height; for (UITouch *touch in touches) { CGPoint p = [touch locationInView:self]; + + // If the ButtonRelease came less than half a second after ButtonPress, + // and didn't move far, then this was a tap, not a drag or a hold. + // Interpret it as "exit". + // + double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) + + ((p.y - tap_point.y) * (p.y - tap_point.y))); + if (tap_time + 0.5 >= double_time() && dist < 20) { + [self stopAndClose:NO]; + return; + } + xe.xany.type = ButtonRelease; xe.xbutton.button = i + 1; xe.xbutton.x = s * p.x; xe.xbutton.y = s * p.y; - rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); + [self rotateMouse: rot_current_angle + x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); xsft->event_cb (xdpy, xwindow, xdata, &xe); i++; + break; // No pinches: only look at the first touch. } } } @@ -1395,11 +1598,16 @@ rotate_mouse (int *x, int *y, int w, int h, int rot) - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + // If they are trying to pinch, just do nothing. + if ([[event allTouches] count] > 1) + return; + if (xsft->event_cb && xwindow) { - double s = self.contentScaleFactor; + double s = [self hackedContentScaleFactor]; XEvent xe; memset (&xe, 0, sizeof(xe)); int i = 0; + // #### 'frame' here or 'bounds'? int w = s * [self frame].size.width; int h = s * [self frame].size.height; for (UITouch *touch in touches) { @@ -1407,10 +1615,12 @@ rotate_mouse (int *x, int *y, int w, int h, int rot) xe.xany.type = MotionNotify; xe.xmotion.x = s * p.x; xe.xmotion.y = s * p.y; - rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); + [self rotateMouse: rot_current_angle + x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y); xsft->event_cb (xdpy, xwindow, xdata, &xe); i++; + break; // No pinches: only look at the first touch. } } } @@ -1418,10 +1628,27 @@ rotate_mouse (int *x, int *y, int w, int h, int rot) /* We need this to respond to "shake" gestures */ -- (BOOL)canBecomeFirstResponder { +- (BOOL)canBecomeFirstResponder +{ return YES; } +- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event +{ +} + + +- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event +{ +} + +/* Shake means exit and launch a new saver. + */ +- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event +{ + [self stopAndClose:YES]; +} + - (void)setScreenLocked:(BOOL)locked { @@ -1449,7 +1676,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]; }