+- (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];
+ }
+
+ 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
+ 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
+
+ // 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 (! 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 addGestureRecognizer: dtap];
+ [self addGestureRecognizer: stap];
+ [self addGestureRecognizer: pan];
+ [self addGestureRecognizer: pan2];
+ [self addGestureRecognizer: hold];
+
+ [dtap release];
+ [stap release];
+ [pan release];
+ [pan2 release];
+ [hold release];
+}
+
+
+
+- (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
+{
+ // 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;
+}
+
+
+/* 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;
+
+ double s = [self hackedContentScaleFactor];
+ XEvent xe;
+ memset (&xe, 0, sizeof(xe));
+
+ 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];
+}
+
+
+/* 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;
+
+ 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];
+}
+
+
+/* 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
+}
+