+- (void) flagsChanged: (NSEvent *) e
+{
+ if (! [self convertEvent:e type:KeyPress])
+ [super flagsChanged:e];
+}
+
+#else // USE_IPHONE
+
+
+- (void) stopAndClose:(Bool)relaunch_p
+{
+ if ([self isAnimating])
+ [self stopAnimation];
+
+ /* Need to make the SaverListController be the firstResponder again
+ so that it can continue to receive its own shake events. I
+ suppose that this abstraction-breakage means that I'm adding
+ XScreenSaverView to the UINavigationController wrong...
+ */
+// UIViewController *v = [[self window] rootViewController];
+// if ([v isKindOfClass: [UINavigationController class]]) {
+// UINavigationController *n = (UINavigationController *) v;
+// [[n topViewController] becomeFirstResponder];
+// }
+ [self resignFirstResponder];
+
+ // Find SaverRunner.window (as opposed to SaverRunner.saverWindow)
+ UIWindow *listWindow = 0;
+ for (UIWindow *w in [[UIApplication sharedApplication] windows]) {
+ if (w != [self window]) {
+ listWindow = w;
+ break;
+ }
+ }
+
+ UIView *fader = [self superview]; // the "backgroundView" view is our parent
+
+ if (relaunch_p) { // Fake a shake on the SaverListController.
+ UIViewController *v = [listWindow rootViewController];
+ if ([v isKindOfClass: [UINavigationController class]]) {
+# if TARGET_IPHONE_SIMULATOR
+ NSLog (@"simulating shake on saver list");
+# endif
+ UINavigationController *n = (UINavigationController *) v;
+ [[n topViewController] motionEnded: UIEventSubtypeMotionShake
+ withEvent: nil];
+ }
+ } else { // Not launching another, animate our return to the list.
+# if TARGET_IPHONE_SIMULATOR
+ NSLog (@"fading back to saver list");
+# endif
+ UIWindow *saverWindow = [self window]; // not SaverRunner.window
+ [listWindow setHidden:NO];
+ [UIView animateWithDuration: 0.5
+ animations:^{ fader.alpha = 0.0; }
+ completion:^(BOOL finished) {
+ [fader removeFromSuperview];
+ fader.alpha = 1.0;
+ [saverWindow setHidden:YES];
+ [listWindow makeKeyAndVisible];
+ [[[listWindow rootViewController] view] becomeFirstResponder];
+ }];
+ }
+}
+
+
+/* Whether the shape of the X11 Window should be changed to HxW when the
+ device is in a landscape orientation. X11 hacks want this, but OpenGL
+ hacks do not.
+ */
+- (BOOL)reshapeRotatedWindow
+{
+ return YES;
+}
+
+
+/* Called after the device's orientation has changed.
+
+ Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways.
+
+ The UI (list of savers, preferences panels) is rotated by the system,
+ because its UIWindow is under a UINavigationController that does
+ automatic rotation, using Core Animation.
+
+ The savers are under a different UIWindow and a UINavigationController
+ that does not do automatic rotation.
+
+ We have to do it this way for OpenGL savers because using Core Animation
+ on an EAGLContext causes the OpenGL pipeline to fall back on software
+ rendering and performance goes to hell.
+
+ For X11-only savers, we could just use Core Animation and let the system
+ handle it, but (maybe) it's simpler to do it the same way for X11 and GL.
+
+ During and after rotation, the size/shape of the X11 window changes,
+ and ConfigureNotify events are generated.
+
+ X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which
+ rotated at the last minute via a CGAffineTransformMakeRotation when it is
+ copied to the display hardware.
+
+ GL code always recieves a portrait-oriented X11 Window whose size never
+ changes. The GL COLOR_BUFFER is displayed on the hardware directly and
+ unrotated, so the GL hacks themselves are responsible for rotating the
+ GL scene to match current_device_rotation().
+
+ Touch events are converted to mouse clicks, and those XEvent coordinates
+ are reported in the coordinate system currently in use by the X11 window.
+ Again, GL must convert those.
+ */
+- (void)didRotate:(NSNotification *)notification
+{
+ UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
+
+ /* If the simulator starts up in the rotated position, sometimes
+ the UIDevice says we're in Portrait when we're not -- but it
+ turns out that the UINavigationController knows what's up!
+ So get it from there.
+ */
+ if (current == UIDeviceOrientationUnknown) {
+ switch ([[[self window] rootViewController] interfaceOrientation]) {
+ case UIInterfaceOrientationPortrait:
+ current = UIDeviceOrientationPortrait;
+ break;
+ case UIInterfaceOrientationPortraitUpsideDown:
+ current = UIDeviceOrientationPortraitUpsideDown;
+ break;
+ /* It's opposite day, "because rotating the device to the left requires
+ rotating the content to the right" */
+ case UIInterfaceOrientationLandscapeLeft:
+ current = UIDeviceOrientationLandscapeRight;
+ break;
+ case UIInterfaceOrientationLandscapeRight:
+ current = UIDeviceOrientationLandscapeLeft;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
+ an orientation change event with an unknown orientation. Those seem
+ to always be immediately followed by another orientation change with
+ a *real* orientation change, so let's try just ignoring those bogus
+ ones and hoping that the real one comes in shortly...
+ */
+ if (current == UIDeviceOrientationUnknown)
+ return;
+
+ if (rotation_ratio >= 0) return; // in the midst of rotation animation
+ if (orientation == current) return; // no change
+
+ // When transitioning to FaceUp or FaceDown, pretend there was no change.
+ if (current == UIDeviceOrientationFaceUp ||
+ current == UIDeviceOrientationFaceDown)
+ return;
+
+ new_orientation = current; // current animation target
+ rotation_ratio = 0; // start animating
+ rot_start_time = double_time();
+
+ switch (orientation) {
+ case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
+ case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
+ case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
+ default: angle_from = 0; break;
+ }
+
+ switch (new_orientation) {
+ case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
+ case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
+ case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
+ default: angle_to = 0; break;
+ }
+
+ switch (orientation) {
+ case UIDeviceOrientationLandscapeRight: // from landscape
+ case UIDeviceOrientationLandscapeLeft:
+ rot_from.width = initial_bounds.height;
+ rot_from.height = initial_bounds.width;
+ break;
+ default: // from portrait
+ rot_from.width = initial_bounds.width;
+ rot_from.height = initial_bounds.height;
+ break;
+ }
+
+ switch (new_orientation) {
+ case UIDeviceOrientationLandscapeRight: // to landscape
+ case UIDeviceOrientationLandscapeLeft:
+ rot_to.width = initial_bounds.height;
+ rot_to.height = initial_bounds.width;
+ break;
+ default: // to portrait
+ rot_to.width = initial_bounds.width;
+ rot_to.height = initial_bounds.height;
+ break;
+ }
+
+# if TARGET_IPHONE_SIMULATOR
+ NSLog (@"rotation begun: %s %d -> %s %d; %d x %d",
+ orientname(orientation), (int) rot_current_angle,
+ orientname(new_orientation), (int) angle_to,
+ (int) rot_current_size.width, (int) rot_current_size.height);
+# endif
+
+ if (! initted_p) {
+ // If we've done a rotation but the saver hasn't been initialized yet,
+ // don't bother going through an X11 resize, but just do it now.
+ rot_start_time = 0; // dawn of time
+ [self hackRotation];
+ }
+}
+
+
+/* We distinguish between taps and drags.
+
+ - Drags/pans (down, motion, up) are sent to the saver to handle.
+ - Single-taps exit the saver.
+ - Double-taps are sent to the saver as a "Space" keypress.
+ - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
+
+ This means a saver cannot respond to a single-tap. Only a few try to.
+ */
+
+- (void)initGestures
+{
+ UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleDoubleTap)];
+ dtap.numberOfTapsRequired = 2;
+ dtap.numberOfTouchesRequired = 1;
+
+ UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleTap)];
+ stap.numberOfTapsRequired = 1;
+ stap.numberOfTouchesRequired = 1;
+
+ UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handlePan:)];
+ pan.maximumNumberOfTouches = 1;
+ pan.minimumNumberOfTouches = 1;
+
+ // I couldn't get Swipe to work, but using a second Pan recognizer works.
+ UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handlePan2:)];
+ pan2.maximumNumberOfTouches = 2;
+ pan2.minimumNumberOfTouches = 2;
+
+ // Also handle long-touch, and treat that the same as Pan.
+ // Without this, panning doesn't start until there's motion, so the trick
+ // of holding down your finger to freeze the scene doesn't work.
+ //
+ UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
+ initWithTarget:self
+ action:@selector(handleLongPress:)];
+ hold.numberOfTapsRequired = 0;
+ hold.numberOfTouchesRequired = 1;
+ hold.minimumPressDuration = 0.25; /* 1/4th second */
+
+ [stap requireGestureRecognizerToFail: dtap];
+ [stap requireGestureRecognizerToFail: hold];
+ [dtap requireGestureRecognizerToFail: hold];
+ [pan requireGestureRecognizerToFail: hold];
+
+ [self setMultipleTouchEnabled:YES];
+
+ [self addGestureRecognizer: dtap];
+ [self addGestureRecognizer: stap];
+ [self addGestureRecognizer: pan];
+ [self addGestureRecognizer: pan2];
+ [self addGestureRecognizer: hold];
+
+ [dtap release];
+ [stap release];
+ [pan release];
+ [pan2 release];
+ [hold release];
+}
+
+
+/* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
+ convert it to what X11 and OpenGL expect.
+ */
+- (void) convertMouse:(int)rot x:(int*)x y:(int *)y
+{
+ int w = [self frame].size.width;
+ int h = [self frame].size.height;
+ int xx = *x, yy = *y;
+ int swap;
+
+ if (ignore_rotation_p) {
+ // We need to rotate the coordinates to match the unrotated X11 window.
+ switch (orientation) {
+ case UIDeviceOrientationLandscapeRight:
+ swap = xx; xx = h-yy; yy = swap;
+ break;
+ case UIDeviceOrientationLandscapeLeft:
+ swap = xx; xx = yy; yy = w-swap;
+ break;
+ case UIDeviceOrientationPortraitUpsideDown:
+ xx = w-xx; yy = h-yy;
+ default:
+ break;
+ }
+ }
+
+ double s = [self contentScaleFactor];
+ *x = xx * s;
+ *y = yy * s;
+
+# if TARGET_IPHONE_SIMULATOR
+ NSLog (@"touch %4d, %-4d in %4d x %-4d %d %d\n",
+ *x, *y, (int)(w*s), (int)(h*s),
+ ignore_rotation_p, [self reshapeRotatedWindow]);
+# endif
+}
+
+
+/* Single click exits saver.
+ */
+- (void) handleTap
+{
+ [self stopAndClose:NO];
+}
+
+
+/* Double click sends Space KeyPress.
+ */
+- (void) handleDoubleTap
+{
+ if (!xsft->event_cb || !xwindow) return;
+
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
+ xe.xkey.keycode = ' ';
+ xe.xany.type = KeyPress;
+ BOOL ok1 = [self sendEvent: &xe];
+ xe.xany.type = KeyRelease;
+ BOOL ok2 = [self sendEvent: &xe];
+ if (!(ok1 || ok2))
+ [self beep];
+}
+
+
+/* Drag with one finger down: send MotionNotify.
+ */
+- (void) handlePan:(UIGestureRecognizer *)sender
+{
+ if (!xsft->event_cb || !xwindow) return;
+
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
+
+ CGPoint p = [sender locationInView:self]; // this is in points, not pixels
+ int x = p.x;
+ int y = p.y;
+ [self convertMouse: rot_current_angle x:&x y:&y];
+ jwxyz_mouse_moved (xdpy, xwindow, x, y);
+
+ switch (sender.state) {
+ case UIGestureRecognizerStateBegan:
+ xe.xany.type = ButtonPress;
+ xe.xbutton.button = 1;
+ xe.xbutton.x = x;
+ xe.xbutton.y = y;
+ break;
+
+ case UIGestureRecognizerStateEnded:
+ xe.xany.type = ButtonRelease;
+ xe.xbutton.button = 1;
+ xe.xbutton.x = x;
+ xe.xbutton.y = y;
+ break;
+
+ case UIGestureRecognizerStateChanged:
+ xe.xany.type = MotionNotify;
+ xe.xmotion.x = x;
+ xe.xmotion.y = y;
+ break;
+
+ default:
+ break;
+ }
+
+ BOOL ok = [self sendEvent: &xe];
+ if (!ok && xe.xany.type == ButtonRelease)
+ [self beep];
+}
+
+
+/* Hold one finger down: assume we're about to start dragging.
+ Treat the same as Pan.
+ */
+- (void) handleLongPress:(UIGestureRecognizer *)sender
+{
+ [self handlePan:sender];
+}
+
+
+
+/* Drag with 2 fingers down: send arrow keys.
+ */
+- (void) handlePan2:(UIPanGestureRecognizer *)sender
+{
+ if (!xsft->event_cb || !xwindow) return;
+
+ if (sender.state != UIGestureRecognizerStateEnded)
+ return;
+
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
+
+ CGPoint p = [sender locationInView:self]; // this is in points, not pixels
+ int x = p.x;
+ int y = p.y;
+ [self convertMouse: rot_current_angle x:&x y:&y];
+
+ if (abs(x) > abs(y))
+ xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
+ else
+ xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
+
+ BOOL ok1 = [self sendEvent: &xe];
+ xe.xany.type = KeyRelease;
+ BOOL ok2 = [self sendEvent: &xe];
+ if (!(ok1 || ok2))
+ [self beep];
+}
+
+
+/* We need this to respond to "shake" gestures
+ */
+- (BOOL)canBecomeFirstResponder
+{
+ return YES;
+}
+
+- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+
+- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+/* Shake means exit and launch a new saver.
+ */
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+ [self stopAndClose:YES];
+}
+
+
+- (void)setScreenLocked:(BOOL)locked
+{
+ if (screenLocked == locked) return;
+ screenLocked = locked;
+ if (locked) {
+ if ([self isAnimating])
+ [self stopAnimation];
+ } else {
+ if (! [self isAnimating])
+ [self startAnimation];
+ }
+}
+
+#endif // USE_IPHONE
+
+
+- (void) checkForUpdates
+{
+# ifndef USE_IPHONE
+ // We only check once at startup, even if there are multiple screens,
+ // and even if this saver is running for many days.
+ // (Uh, except this doesn't work because this static isn't shared,
+ // even if we make it an exported global. Not sure why. Oh well.)
+ static BOOL checked_p = NO;
+ if (checked_p) return;
+ checked_p = YES;
+
+ // If it's off, don't bother running the updater. Otherwise, the
+ // updater will decide if it's time to hit the network.
+ if (! get_boolean_resource (xdpy,
+ SUSUEnableAutomaticChecksKey,
+ SUSUEnableAutomaticChecksKey))
+ return;
+
+ NSString *updater = @"XScreenSaverUpdater.app";
+
+ // There may be multiple copies of the updater: e.g., one in /Applications
+ // and one in the mounted installer DMG! It's important that we run the
+ // one from the disk and not the DMG, so search for the right one.
+ //
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+ NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+ NSArray *search =
+ @[[[bundle bundlePath] stringByDeletingLastPathComponent],
+ [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
+ @"/Library/Screen Savers",
+ @"/System/Library/Screen Savers",
+ @"/Applications",
+ @"/Applications/Utilities"];
+ NSString *app_path = nil;
+ for (NSString *dir in search) {
+ NSString *p = [dir stringByAppendingPathComponent:updater];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
+ app_path = p;
+ break;
+ }
+ }
+
+ if (! app_path)
+ app_path = [workspace fullPathForApplication:updater];
+
+ if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
+ app_path = 0; // The DMG version will not do.
+
+ if (!app_path) {
+ NSLog(@"Unable to find %@", updater);
+ return;
+ }
+
+ NSError *err = nil;
+ if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
+ options:(NSWorkspaceLaunchWithoutAddingToRecents |
+ NSWorkspaceLaunchWithoutActivation |
+ NSWorkspaceLaunchAndHide)
+ configuration:nil
+ error:&err]) {
+ NSLog(@"Unable to launch %@: %@", app_path, err);
+ }
+
+# endif // !USE_IPHONE
+}
+