+- (void) flagsChanged: (NSEvent *) e
+{
+ if (! [self doEvent: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];
+ }
+}
+
+
+/* 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]
+
+ 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.
+ */
+
+- (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;
+}
+
+
+#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 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.
+ }
+ }
+}
+
+
+- (void)touchesEnded:(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 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;
+ [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.
+ }
+ }
+}
+
+
+- (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 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.
+ }
+ }
+}
+
+
+/* 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
+}
+