X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?p=xscreensaver;a=blobdiff_plain;f=OSX%2FXScreenSaverView.m;h=8c6b5ea4762321c225db7e2f49dc23ef8058d323;hp=404cfc1c42bc5c163592278cf5e8c90d5b282544;hb=f8cf5ac7b2f53510f80a0eaf286a25298be17bfe;hpb=ec8d2b32b63649e6d32bdfb306eda062769af823 diff --git a/OSX/XScreenSaverView.m b/OSX/XScreenSaverView.m index 404cfc1c..8c6b5ea4 100644 --- a/OSX/XScreenSaverView.m +++ b/OSX/XScreenSaverView.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski +/* xscreensaver, Copyright (c) 2006-2012 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 @@ -15,6 +15,7 @@ the "screenhack.c" module. */ +#import #import "XScreenSaverView.h" #import "XScreenSaverConfigSheet.h" #import "screenhackI.h" @@ -41,19 +42,66 @@ const char *progclass; int mono_p = 0; +# ifdef USE_IPHONE + +/* 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 + + + @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, @@ -62,11 +110,18 @@ int mono_p = 0; CFRelease (url); NSAssert1 (cfb, @"no CFBundle for \"%@\"", path); + if (! name) + name = [[path lastPathComponent] stringByDeletingPathExtension]; + + NSString *table_name = [[name lowercaseString] + stringByAppendingString: + @"_xscreensaver_function_table"]; void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name); - NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path); CFRelease (cfb); -// NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr); + if (! addr) + NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path); + return (struct xscreensaver_function_table *) addr; } @@ -127,7 +182,7 @@ 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 }, @@ -148,7 +203,11 @@ add_default_options (const XrmOptionDescRec *opts, ".doFPS: False", ".doubleBuffer: True", ".multiSample: False", +# ifndef USE_IPHONE ".textMode: date", +# else + ".textMode: url", +# endif // ".textLiteral: ", // ".textFile: ", ".textURL: http://twitter.com/statuses/public_timeline.atom", @@ -156,6 +215,7 @@ add_default_options (const XrmOptionDescRec *opts, ".grabDesktopImages: yes", ".chooseRandomImages: no", ".imageDirectory: ~/Pictures", + ".relaunchDelay: 2", 0 }; @@ -209,14 +269,53 @@ 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 + + +- (id) initWithFrame:(NSRect)frame + saverName:(NSString *)saverName + isPreview:(BOOL)isPreview { +# ifdef USE_IPHONE + 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]. + rotation_ratio = -1; +# endif + if (! (self = [super initWithFrame:frame isPreview:isPreview])) return 0; - xsft = [self findFunctionTable]; + xsft = [self findFunctionTable: saverName]; + if (! xsft) { + [self release]; + return 0; + } + [self setShellPath]; +# ifdef USE_IPHONE + [self setMultipleTouchEnabled:YES]; + orientation = UIDeviceOrientationUnknown; + [self didRotate:nil]; +# endif // USE_IPHONE + setup_p = YES; if (xsft->setup_cb) xsft->setup_cb (xsft, xsft->setup_arg); @@ -244,16 +343,40 @@ add_default_options (const XrmOptionDescRec *opts, next_frame_time = 0; +# ifdef USE_IPHONE + [self createBackbuffer]; + + // So we can tell when we're docked. + [UIDevice currentDevice].batteryMonitoringEnabled = YES; +# endif // USE_IPHONE + return self; } +- (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); + +# ifdef USE_IPHONE + if (backbuffer) + CGContextRelease (backbuffer); +# endif + [prefsReader release]; + + // xsft + // fpst + // orientation_timer + [super dealloc]; } @@ -263,6 +386,28 @@ 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 +{ + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + [prefs setBool:YES forKey:@"wasRunning"]; + NSAssert (timer == crash_timer, @"crash timer screwed up"); + crash_timer = 0; +} +#endif // USE_IPHONE + + - (void) startAnimation { NSAssert(![self isAnimating], @"already animating"); @@ -272,8 +417,27 @@ add_default_options (const XrmOptionDescRec *opts, initialization of the screen saver (xsft->init_cb) in the first call to animateOneFrame() instead. */ + +# ifdef USE_IPHONE + if (crash_timer) + [crash_timer invalidate]; + 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); +# endif } + - (void)stopAnimation { NSAssert([self isAnimating], @"not animating"); @@ -297,7 +461,22 @@ add_default_options (const XrmOptionDescRec *opts, xdata = 0; } +# ifdef USE_IPHONE + if (crash_timer) + [crash_timer invalidate]; + crash_timer = 0; + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + [prefs removeObjectForKey:@"wasRunning"]; +# 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; +# endif } @@ -321,14 +500,143 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) fps_draw (fpst); } +#ifdef USE_IPHONE -- (void) animateOneFrame +/* Create a bitmap context into which we render everything. + */ +- (void) createBackbuffer { + 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); + } +} + +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) { // Done animating. + orientation = new_orientation; + rot_current_angle = angle_to; + rot_current_size = rot_to; + rotation_ratio = -1; + + // 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 + + 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]]; +} + +#endif // USE_IPHONE + + +- (void) render_x11 +{ +# ifdef USE_IPHONE + if (orientation == UIDeviceOrientationUnknown) + [self didRotate:nil]; + [self hackRotation]; +# endif + if (!initted_p) { if (! xdpy) { - xdpy = jwxyz_make_display (self); +# ifdef USE_IPHONE + 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 } if (!setup_p) { @@ -348,7 +656,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. @@ -376,11 +686,14 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) } } + /* 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) @@ -388,8 +701,9 @@ 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); } @@ -421,32 +735,50 @@ 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_IPHONE + r = [self frame]; +# else // USE_IPHONE + r.origin.x = 0; + r.origin.y = 0; + r.size.width = backbuffer_size.width; + r.size.height = backbuffer_size.height; +# endif // USE_IPHONE + 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.) // jwxyz_sources_run (display_sources_data (xdpy)); + // And finally: // +# ifndef USE_IPHONE NSDisableScreenUpdates(); +# endif 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 @@ -480,6 +812,12 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) } +/* 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. + */ +#ifndef USE_IPHONE + - (void)drawRect:(NSRect)rect { if (xwindow) // clear to the X window's bg color, not necessarily black. @@ -488,21 +826,121 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) [super drawRect:rect]; // early: black. } +- (void) animateOneFrame +{ + [self render_x11]; +} -- (void) setFrameSize:(NSSize) newSize +#else // USE_IPHONE + +- (void)drawRect:(NSRect)rect { - [super setFrameSize:newSize]; - if ([self isAnimating]) { - resized_p = YES; + // 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; + + // 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); + + // Flip Y axis + t = CGAffineTransformConcat (t, + CGAffineTransformMake ( 1, 0, 0, + -1, 0, frame.size.height)); + + // Clear background (visible in corners of screen during rotation) + if (rotation_ratio != -1) { + CGContextSetGrayFillColor (cgc, 0, 1); + CGContextFillRect (cgc, frame); } + + CGContextConcatCTM (cgc, t); + + // Copy the backbuffer to the screen. + // Note that CGContextDrawImage measures in "points", not "pixels". + CGImageRef img = CGBitmapContextCreateImage (backbuffer); + CGContextDrawImage (cgc, target, img); + CGImageRelease (img); +} + +- (void) animateOneFrame +{ + [self setNeedsDisplay]; } +#endif // !USE_IPHONE + + + - (void) setFrame:(NSRect) newRect { [super setFrame:newRect]; - if (xwindow) // inform Xlib that the window has changed. - jwxyz_window_resized (xdpy, xwindow); + +# 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 + } +} + + +# ifndef USE_IPHONE // Doesn't exist on iOS +- (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 } +# endif // !USE_IPHONE +(BOOL) performGammaFade @@ -515,7 +953,11 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) return YES; } +#ifndef USE_IPHONE - (NSWindow *) configureSheet +#else +- (UIViewController *) configureView +#endif { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *file = [NSString stringWithCString:xsft->progclass @@ -528,11 +970,18 @@ 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 + + sheet = [[XScreenSaverConfigSheet alloc] + initWithXMLFile:path + options:xsft->options + controller:[prefsReader userDefaultsController] + 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]; @@ -555,6 +1004,8 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) } +#ifndef USE_IPHONE + /* Convert an NSEvent into an XEvent, and pass it along. Returns YES if it was handled. */ @@ -565,7 +1016,7 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) ![self isAnimating] || !initted_p) return NO; - + XEvent xe; memset (&xe, 0, sizeof(xe)); @@ -580,9 +1031,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.contentScaleFactor; +# else + int s = 1; +# endif + int x = s * p.x; + int y = s * ([self frame].size.height - p.y); + xe.xany.type = type; switch (type) { case ButtonPress: @@ -607,9 +1063,47 @@ 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; + } + } + + xe.xkey.keycode = k; xe.xkey.state = state; break; } @@ -685,6 +1179,266 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) [super keyUp:e]; } +- (void) flagsChanged: (NSEvent *) e +{ + if (! [self doEvent:e type:KeyPress]) + [super flagsChanged:e]; +} + +#else // USE_IPHONE + + +/* Called after the device's orientation has changed. + + Note: we could include a subclass of UIViewController which + contains a shouldAutorotateToInterfaceOrientation method that + returns YES, in which case Core Animation would auto-rotate our + View for us in response to rotation events... but, that interacts + badly with the EAGLContext -- if you introduce Core Animation into + the path, the OpenGL pipeline probably falls back on software + rendering and performance goes to hell. Also, the scaling and + rotation that Core Animation does interacts incorrectly with the GL + context anyway. + + So, we have to hack the rotation animation manually, in the GL world. + + Possibly XScreenSaverView should use Core Animation, and + XScreenSaverGLView should override that. +*/ +- (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; + case UIInterfaceOrientationLandscapeLeft: // It's opposite day + 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 + + 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; + } + + 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; + } + + NSRect ff = [self frame]; + + switch (orientation) { + case UIInterfaceOrientationLandscapeLeft: // from landscape + case UIInterfaceOrientationLandscapeRight: + rot_from.width = ff.size.height; + rot_from.height = ff.size.width; + break; + default: // from portrait + rot_from.width = ff.size.width; + rot_from.height = ff.size.height; + break; + } + + switch (new_orientation) { + case UIInterfaceOrientationLandscapeLeft: // to landscape + case UIInterfaceOrientationLandscapeRight: + rot_to.width = ff.size.height; + rot_to.height = ff.size.width; + break; + default: // to portrait + rot_to.width = ff.size.width; + rot_to.height = ff.size.height; + break; + } + + 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]; + } +} + + +/* In the simulator, multi-touch sequences look like this: + + touchesBegan [touchA, touchB] + touchesEnd [touchA, touchB] + + But on real devices, sometimes you get that, but sometimes you get: + + touchesBegan [touchA, touchB] + touchesEnd [touchB] + touchesEnd [touchA] + + Or even + + 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. + */ + +static void +rotate_mouse (int *x, int *y, int w, int h, int rot) +{ + 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; } +} + + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (xsft->event_cb && xwindow) { + double s = self.contentScaleFactor; + XEvent xe; + memset (&xe, 0, sizeof(xe)); + int i = 0; + 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; + rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); + jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); + xsft->event_cb (xdpy, xwindow, xdata, &xe); + i++; + } + } +} + + +- (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 (xsft->event_cb && xwindow) { + double s = self.contentScaleFactor; + XEvent xe; + memset (&xe, 0, sizeof(xe)); + int i = 0; + 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 = 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); + jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); + xsft->event_cb (xdpy, xwindow, xdata, &xe); + i++; + } + } +} + + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (xsft->event_cb && xwindow) { + double s = self.contentScaleFactor; + XEvent xe; + memset (&xe, 0, sizeof(xe)); + int i = 0; + 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; + rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); + jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y); + xsft->event_cb (xdpy, xwindow, xdata, &xe); + i++; + } + } +} + + +/* We need this to respond to "shake" gestures + */ +- (BOOL)canBecomeFirstResponder { + return 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 + @end