1 /* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski <jwz@jwz.org>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
12 /* This is a subclass of Apple's ScreenSaverView that knows how to run
13 xscreensaver programs without X11 via the dark magic of the "jwxyz"
14 library. In xscreensaver terminology, this is the replacement for
15 the "screenhack.c" module.
18 #import <QuartzCore/QuartzCore.h>
19 #import "XScreenSaverView.h"
20 #import "XScreenSaverConfigSheet.h"
21 #import "screenhackI.h"
22 #import "xlockmoreI.h"
23 #import "jwxyz-timers.h"
25 /* Garbage collection only exists if we are being compiled against the
26 10.6 SDK or newer, not if we are building against the 10.4 SDK.
28 #ifndef MAC_OS_X_VERSION_10_6
29 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
31 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */
32 # import <objc/objc-auto.h>
33 # define DO_GC_HACKERY
36 extern struct xscreensaver_function_table *xscreensaver_function_table;
38 /* Global variables used by the screen savers
41 const char *progclass;
47 /* Stub definition of the superclass, for iPhone.
49 @implementation ScreenSaverView
51 NSTimeInterval anim_interval;
56 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
57 self = [super initWithFrame:frame];
59 anim_interval = 1.0/30;
62 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
63 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
64 - (BOOL)hasConfigureSheet { return NO; }
65 - (NSWindow *)configureSheet { return nil; }
66 - (NSView *)configureView { return nil; }
67 - (BOOL)isPreview { return NO; }
68 - (BOOL)isAnimating { return animating_p; }
69 - (void)animateOneFrame { }
71 - (void)startAnimation {
72 if (animating_p) return;
74 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
76 selector:@selector(animateOneFrame)
81 - (void)stopAnimation {
83 [anim_timer invalidate];
90 # endif // !USE_IPHONE
94 @implementation XScreenSaverView
96 // Given a lower-cased saver name, returns the function table for it.
97 // If no name, guess the name from the class's bundle name.
99 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
101 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
102 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
104 NSString *path = [nsb bundlePath];
105 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
107 kCFURLPOSIXPathStyle,
109 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
111 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
114 name = [[path lastPathComponent] stringByDeletingPathExtension];
116 NSString *table_name = [[name lowercaseString]
117 stringByAppendingString:
118 @"_xscreensaver_function_table"];
119 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
123 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
125 return (struct xscreensaver_function_table *) addr;
129 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
130 // to $PATH for the benefit of savers that include helper shell scripts.
132 - (void) setShellPath
134 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
135 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
137 NSString *nsdir = [nsb resourcePath];
138 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
139 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
140 const char *opath = getenv ("PATH");
141 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
142 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
143 strcpy (npath, "PATH=");
146 strcat (npath, opath);
147 if (putenv (npath)) {
152 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
156 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
157 // (e.g., "xscreensaver-text") know how to look up resources.
159 - (void) setResourcesEnv:(NSString *) name
161 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
162 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
164 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
165 char *env = (char *) malloc (strlen (s) + 40);
166 strcpy (env, "XSCREENSAVER_CLASSPATH=");
172 /* Don't free (env) -- MacOS's putenv() does not copy it. */
177 add_default_options (const XrmOptionDescRec *opts,
178 const char * const *defs,
179 XrmOptionDescRec **opts_ret,
180 const char ***defs_ret)
182 /* These aren't "real" command-line options (there are no actual command-line
183 options in the Cocoa version); but this is the somewhat kludgey way that
184 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
185 ../hacks/config/\*.xml files communicate with the preferences database.
187 static const XrmOptionDescRec default_options [] = {
188 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
189 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
190 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
191 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
192 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
193 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
194 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
195 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
196 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
197 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
198 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
199 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
202 static const char *default_defaults [] = {
204 ".doubleBuffer: True",
205 ".multiSample: False",
213 ".textURL: http://twitter.com/statuses/public_timeline.atom",
215 ".grabDesktopImages: yes",
216 ".chooseRandomImages: no",
217 ".imageDirectory: ~/Pictures",
223 for (i = 0; default_options[i].option; i++)
225 for (i = 0; opts[i].option; i++)
228 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
229 calloc (count + 1, sizeof (*opts2));
233 while (default_options[j].option) {
234 opts2[i] = default_options[j];
238 while (opts[j].option) {
249 for (i = 0; default_defaults[i]; i++)
251 for (i = 0; defs[i]; i++)
254 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
258 while (default_defaults[j]) {
259 defs2[i] = default_defaults[j];
273 /* Returns the current time in seconds as a double.
279 # ifdef GETTIMEOFDAY_TWO_ARGS
281 gettimeofday(&now, &tzp);
286 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
291 - (id) initWithFrame:(NSRect)frame
292 saverName:(NSString *)saverName
293 isPreview:(BOOL)isPreview
296 rot_current_size = frame.size; // needs to be early, because
297 rot_from = rot_current_size; // [self setFrame] is called by
298 rot_to = rot_current_size; // [super initWithFrame].
302 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
305 xsft = [self findFunctionTable: saverName];
314 [self setMultipleTouchEnabled:YES];
315 orientation = UIDeviceOrientationUnknown;
316 [self didRotate:nil];
317 # endif // USE_IPHONE
321 xsft->setup_cb (xsft, xsft->setup_arg);
323 /* The plist files for these preferences show up in
324 $HOME/Library/Preferences/ByHost/ in a file named like
325 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
327 NSString *name = [NSString stringWithCString:xsft->progclass
328 encoding:NSUTF8StringEncoding];
329 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
330 [self setResourcesEnv:name];
333 XrmOptionDescRec *opts = 0;
334 const char **defs = 0;
335 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
336 prefsReader = [[PrefsReader alloc]
337 initWithName:name xrmKeys:opts defaults:defs];
339 // free (opts); // bah, we need these! #### leak!
340 xsft->options = opts;
342 progname = progclass = xsft->progclass;
347 [self createBackbuffer];
349 // So we can tell when we're docked.
350 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
351 # endif // USE_IPHONE
356 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
358 return [self initWithFrame:frame saverName:0 isPreview:p];
364 NSAssert(![self isAnimating], @"still animating");
365 NSAssert(!xdata, @"xdata not yet freed");
367 jwxyz_free_display (xdpy);
371 CGContextRelease (backbuffer);
374 [prefsReader release];
383 - (PrefsReader *) prefsReader
390 - (void) lockFocus { }
391 - (void) unlockFocus { }
397 /* A few seconds after the saver launches, we store the "wasRunning"
398 preference. This is so that if the saver is crashing at startup,
399 we don't launch it again next time, getting stuck in a crash loop.
401 - (void) allSystemsGo: (NSTimer *) timer
403 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
404 [prefs setBool:YES forKey:@"wasRunning"];
405 NSAssert (timer == crash_timer, @"crash timer screwed up");
411 - (void) startAnimation
413 NSAssert(![self isAnimating], @"already animating");
414 NSAssert(!initted_p && !xdata, @"already initialized");
415 [super startAnimation];
416 /* We can't draw on the window from this method, so we actually do the
417 initialization of the screen saver (xsft->init_cb) in the first call
418 to animateOneFrame() instead.
423 [crash_timer invalidate];
424 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
426 selector:@selector(allSystemsGo:)
429 # endif // USE_IPHONE
431 // Never automatically turn the screen off if we are docked,
432 // and an animation is running.
435 [UIApplication sharedApplication].idleTimerDisabled =
436 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
441 - (void)stopAnimation
443 NSAssert([self isAnimating], @"not animating");
447 [self lockFocus]; // in case something tries to draw from here
448 [self prepareContext];
450 /* I considered just not even calling the free callback at all...
451 But webcollage-cocoa needs it, to kill the inferior webcollage
452 processes (since the screen saver framework never generates a
453 SIGPIPE for them...) Instead, I turned off the free call in
454 xlockmore.c, which is where all of the bogus calls are anyway.
456 xsft->free_cb (xdpy, xwindow, xdata);
459 // setup_p = NO; // #### wait, do we need this?
466 [crash_timer invalidate];
468 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
469 [prefs removeObjectForKey:@"wasRunning"];
470 # endif // USE_IPHONE
472 [super stopAnimation];
474 // When an animation is no longer running (e.g., looking at the list)
475 // then it's ok to power off the screen when docked.
478 [UIApplication sharedApplication].idleTimerDisabled = NO;
483 /* Hook for the XScreenSaverGLView subclass
485 - (void) prepareContext
489 /* Hook for the XScreenSaverGLView subclass
491 - (void) resizeContext
497 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
499 fps_compute (fpst, 0, -1);
505 /* Create a bitmap context into which we render everything.
507 - (void) createBackbuffer
509 CGContextRef ob = backbuffer;
510 NSSize osize = backbuffer_size;
512 CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
513 double s = self.contentScaleFactor;
514 backbuffer_size.width = (int) (s * rot_current_size.width);
515 backbuffer_size.height = (int) (s * rot_current_size.height);
516 backbuffer = CGBitmapContextCreate (NULL,
517 backbuffer_size.width,
518 backbuffer_size.height,
520 backbuffer_size.width * 4,
522 kCGImageAlphaPremultipliedLast);
523 NSAssert (backbuffer, @"unable to allocate back buffer");
524 CGColorSpaceRelease (cs);
527 CGContextSetGrayFillColor (backbuffer, 0, 1);
528 CGRect r = CGRectZero;
529 r.size = backbuffer_size;
530 CGContextFillRect (backbuffer, r);
533 // Restore old bits, as much as possible, to the X11 upper left origin.
536 rect.origin.y = (backbuffer_size.height - osize.height);
538 CGImageRef img = CGBitmapContextCreateImage (ob);
539 CGContextDrawImage (backbuffer, rect, img);
540 CGImageRelease (img);
541 CGContextRelease (ob);
545 static GLfloat _global_rot_current_angle_kludge;
547 double current_device_rotation (void)
549 return -_global_rot_current_angle_kludge;
553 - (void) hackRotation
555 if (rotation_ratio >= 0) { // in the midst of a rotation animation
557 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
558 GLfloat f = angle_from;
559 GLfloat t = angle_to;
562 GLfloat dist = -(t-f);
565 // Intermediate angle.
566 rot_current_angle = f - rotation_ratio * dist;
568 // Intermediate frame size.
569 rot_current_size.width = rot_from.width +
570 rotation_ratio * (rot_to.width - rot_from.width);
571 rot_current_size.height = rot_from.height +
572 rotation_ratio * (rot_to.height - rot_from.height);
574 // Tick animation. Complete rotation in 1/6th sec.
575 double now = double_time();
576 double duration = 1/6.0;
577 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
579 if (rotation_ratio > 1) { // Done animating.
580 orientation = new_orientation;
581 rot_current_angle = angle_to;
582 rot_current_size = rot_to;
585 // Check orientation again in case we rotated again while rotating:
586 // this is a no-op if nothing has changed.
587 [self didRotate:nil];
589 } else { // Not animating a rotation.
590 rot_current_angle = angle_to;
591 rot_current_size = rot_to;
594 CLAMP180(rot_current_angle);
595 _global_rot_current_angle_kludge = rot_current_angle;
599 double s = self.contentScaleFactor;
600 if (((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
601 (int) backbuffer_size.height != (int) (s * rot_current_size.height))
602 /* && rotation_ratio == -1*/)
603 [self setFrame:[self frame]];
612 if (orientation == UIDeviceOrientationUnknown)
613 [self didRotate:nil];
621 NSAssert (backbuffer, @"no back buffer");
622 xdpy = jwxyz_make_display (self, backbuffer);
624 xdpy = jwxyz_make_display (self, 0);
626 xwindow = XRootWindow (xdpy, 0);
629 jwxyz_window_resized (xdpy, xwindow,
631 backbuffer_size.width, backbuffer_size.height,
634 NSRect r = [self frame];
635 jwxyz_window_resized (xdpy, xwindow,
636 r.origin.x, r.origin.y,
637 r.size.width, r.size.height,
645 xsft->setup_cb (xsft, xsft->setup_arg);
649 NSAssert(!xdata, @"xdata already initialized");
654 XSetWindowBackground (xdpy, xwindow,
655 get_pixel_resource (xdpy, 0,
656 "background", "Background"));
657 XClearWindow (xdpy, xwindow);
660 [[self window] setAcceptsMouseMovedEvents:YES];
663 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
664 drawing primitives will run on the GPU instead of the CPU.
665 It seems like it might make things worse rather than better,
666 though... Plus it makes us binary-incompatible with 10.4.
668 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
669 [[self window] setPreferredBackingLocation:
670 NSWindowBackingLocationVideoMemory];
674 /* Kludge: even though the init_cb functions are declared to take 2 args,
675 actually call them with 3, for the benefit of xlockmore_init() and
678 void *(*init_cb) (Display *, Window, void *) =
679 (void *(*) (Display *, Window, void *)) xsft->init_cb;
681 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
683 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
684 fpst = fps_init (xdpy, xwindow);
685 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
690 /* I don't understand why we have to do this *every frame*, but we do,
691 or else the cursor comes back on.
694 if (![self isPreview])
695 [NSCursor setHiddenUntilMouseMoves:YES];
701 /* This is just a guess, but the -fps code wants to know how long
702 we were sleeping between frames.
704 long usecs = 1000000 * [self animationTimeInterval];
705 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
706 if (usecs < 0) usecs = 0;
707 fps_slept (fpst, usecs);
711 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
712 This is bad, because some of the screen hacks want to delay for long
713 periods (like 5 seconds or a minute!) between frames, and running them
714 all at 60 FPS is no good.
716 So, we don't use setAnimationTimeInterval, and just let the framework call
717 us whenever. But, we only invoke the screen hack's "draw frame" method
718 when enough time has expired.
720 This means two extra calls to gettimeofday() per frame. For fast-cycling
721 screen savers, that might actually slow them down. Oh well.
723 #### Also, we do not run the draw callback faster than the system's
724 animationTimeInterval, so if any savers are pickier about timing
725 than that, this may slow them down too much. If that's a problem,
726 then we could call draw_cb in a loop here (with usleep) until the
727 next call would put us past animationTimeInterval... But a better
728 approach would probably be to just change the saver to not do that.
731 gettimeofday (&tv, 0);
732 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
733 if (now < next_frame_time) return;
735 [self prepareContext];
738 // We do this here instead of in setFrame so that all the
739 // Xlib drawing takes place under the animation timer.
740 [self resizeContext];
747 r.size.width = backbuffer_size.width;
748 r.size.height = backbuffer_size.height;
749 # endif // USE_IPHONE
751 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
756 // Run any XtAppAddInput callbacks now.
757 // (Note that XtAppAddTimeOut callbacks have already been run by
758 // the Cocoa event loop.)
760 jwxyz_sources_run (display_sources_data (xdpy));
766 NSDisableScreenUpdates();
768 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
769 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
771 NSEnableScreenUpdates();
774 gettimeofday (&tv, 0);
775 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
776 next_frame_time = now + (delay / 1000000.0);
778 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
779 if (delay < [self animationTimeInterval])
780 [self setAnimationTimeInterval:(delay / 1000000.0)];
783 # ifdef DO_GC_HACKERY
784 /* Current theory is that the 10.6 garbage collector sucks in the
787 It only does a collection when a threshold of outstanding
788 collectable allocations has been surpassed. However, CoreGraphics
789 creates lots of small collectable allocations that contain pointers
790 to very large non-collectable allocations: a small CG object that's
791 collectable referencing large malloc'd allocations (non-collectable)
792 containing bitmap data. So the large allocation doesn't get freed
793 until GC collects the small allocation, which triggers its finalizer
794 to run which frees the large allocation. So GC is deciding that it
795 doesn't really need to run, even though the process has gotten
796 enormous. GC eventually runs once pageouts have happened, but by
797 then it's too late, and the machine's resident set has been
800 So, we force an exhaustive garbage collection in this process
801 approximately every 5 seconds whether the system thinks it needs
808 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
811 # endif // DO_GC_HACKERY
815 /* On MacOS: drawRect does nothing, and animateOneFrame renders.
816 On iOS GL: drawRect does nothing, and animateOneFrame renders.
817 On iOS X11: drawRect renders, and animateOneFrame marks the view dirty.
821 - (void)drawRect:(NSRect)rect
823 if (xwindow) // clear to the X window's bg color, not necessarily black.
824 XClearWindow (xdpy, xwindow);
826 [super drawRect:rect]; // early: black.
829 - (void) animateOneFrame
836 - (void)drawRect:(NSRect)rect
838 // Render X11 into the backing store bitmap...
840 NSAssert (backbuffer, @"no back buffer");
841 UIGraphicsPushContext (backbuffer);
843 UIGraphicsPopContext();
845 // Then copy that bitmap to the screen.
847 CGContextRef cgc = UIGraphicsGetCurrentContext();
849 // Mask it to only update the parts that are exposed.
850 // CGContextClipToRect (cgc, rect);
852 double s = self.contentScaleFactor;
853 CGRect frame = [self frame];
856 target.size.width = backbuffer_size.width;
857 target.size.height = backbuffer_size.height;
858 target.origin.x = (s * frame.size.width - target.size.width) / 2;
859 target.origin.y = (s * frame.size.height - target.size.height) / 2;
861 target.origin.x /= s;
862 target.origin.y /= s;
863 target.size.width /= s;
864 target.size.height /= s;
866 CGAffineTransform t = CGAffineTransformIdentity;
868 // Rotate around center
869 float cx = frame.size.width / 2;
870 float cy = frame.size.height / 2;
871 t = CGAffineTransformTranslate (t, cx, cy);
872 t = CGAffineTransformRotate (t, -rot_current_angle / (180.0 / M_PI));
873 t = CGAffineTransformTranslate (t, -cx, -cy);
876 t = CGAffineTransformConcat (t,
877 CGAffineTransformMake ( 1, 0, 0,
878 -1, 0, frame.size.height));
880 // Clear background (visible in corners of screen during rotation)
881 if (rotation_ratio != -1) {
882 CGContextSetGrayFillColor (cgc, 0, 1);
883 CGContextFillRect (cgc, frame);
886 CGContextConcatCTM (cgc, t);
888 // Copy the backbuffer to the screen.
889 // Note that CGContextDrawImage measures in "points", not "pixels".
890 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
891 CGContextDrawImage (cgc, target, img);
892 CGImageRelease (img);
895 - (void) animateOneFrame
897 [self setNeedsDisplay];
900 #endif // !USE_IPHONE
904 - (void) setFrame:(NSRect) newRect
906 [super setFrame:newRect];
909 [self createBackbuffer];
912 resized_p = YES; // The reshape_cb runs in render_x11
913 if (xwindow) { // inform Xlib that the window has changed now.
915 NSAssert (backbuffer, @"no back buffer");
916 // The backbuffer is the rotated size, and so is the xwindow.
917 jwxyz_window_resized (xdpy, xwindow,
919 backbuffer_size.width, backbuffer_size.height,
922 jwxyz_window_resized (xdpy, xwindow,
923 newRect.origin.x, newRect.origin.y,
924 newRect.size.width, newRect.size.height,
931 # ifndef USE_IPHONE // Doesn't exist on iOS
932 - (void) setFrameSize:(NSSize) newSize
934 [super setFrameSize:newSize];
937 jwxyz_window_resized (xdpy, xwindow,
938 [self frame].origin.x,
939 [self frame].origin.y,
940 newSize.width, newSize.height,
941 0); // backbuffer only on iPhone
943 # endif // !USE_IPHONE
946 +(BOOL) performGammaFade
951 - (BOOL) hasConfigureSheet
957 - (NSWindow *) configureSheet
959 - (UIViewController *) configureView
962 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
963 NSString *file = [NSString stringWithCString:xsft->progclass
964 encoding:NSUTF8StringEncoding];
965 file = [file lowercaseString];
966 NSString *path = [bundle pathForResource:file ofType:@"xml"];
968 NSLog (@"%@.xml does not exist in the application bundle: %@/",
969 file, [bundle resourcePath]);
974 UIViewController *sheet;
975 # else // !USE_IPHONE
977 # endif // !USE_IPHONE
979 sheet = [[XScreenSaverConfigSheet alloc]
981 options:xsft->options
982 controller:[prefsReader userDefaultsController]
983 defaults:[prefsReader defaultOptions]];
985 // #### am I expected to retain this, or not? wtf.
986 // I thought not, but if I don't do this, we (sometimes) crash.
993 - (NSUserDefaultsController *) userDefaultsController
995 return [prefsReader userDefaultsController];
999 /* Announce our willingness to accept keyboard input.
1001 - (BOOL)acceptsFirstResponder
1009 /* Convert an NSEvent into an XEvent, and pass it along.
1010 Returns YES if it was handled.
1012 - (BOOL) doEvent: (NSEvent *) e
1015 if (![self isPreview] || // no event handling if actually screen-saving!
1016 ![self isAnimating] ||
1021 memset (&xe, 0, sizeof(xe));
1025 int flags = [e modifierFlags];
1026 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1027 if (flags & NSShiftKeyMask) state |= ShiftMask;
1028 if (flags & NSControlKeyMask) state |= ControlMask;
1029 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1030 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1032 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1035 double s = self.contentScaleFactor;
1040 int y = s * ([self frame].size.height - p.y);
1042 xe.xany.type = type;
1048 xe.xbutton.state = state;
1049 if ([e type] == NSScrollWheel)
1050 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1051 [e deltaY] < 0 ? Button5 :
1052 [e deltaX] > 0 ? Button6 :
1053 [e deltaX] < 0 ? Button7 :
1056 xe.xbutton.button = [e buttonNumber] + 1;
1061 xe.xmotion.state = state;
1066 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1067 [e charactersIgnoringModifiers]);
1070 if (!ns || [ns length] == 0) // dead key
1072 // Cocoa hides the difference between left and right keys.
1073 // Also we only get KeyPress events for these, no KeyRelease
1074 // (unless we hack the mod state manually. Bleh.)
1076 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1077 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1078 else if (flags & NSControlKeyMask) k = XK_Control_L;
1079 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1080 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1082 else if ([ns length] == 1) // real key
1084 switch ([ns characterAtIndex:0]) {
1085 case NSLeftArrowFunctionKey: k = XK_Left; break;
1086 case NSRightArrowFunctionKey: k = XK_Right; break;
1087 case NSUpArrowFunctionKey: k = XK_Up; break;
1088 case NSDownArrowFunctionKey: k = XK_Down; break;
1089 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1090 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1091 case NSHomeFunctionKey: k = XK_Home; break;
1092 case NSPrevFunctionKey: k = XK_Prior; break;
1093 case NSNextFunctionKey: k = XK_Next; break;
1094 case NSBeginFunctionKey: k = XK_Begin; break;
1095 case NSEndFunctionKey: k = XK_End; break;
1099 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1100 k = (s && *s ? *s : 0);
1106 xe.xkey.keycode = k;
1107 xe.xkey.state = state;
1115 [self prepareContext];
1116 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1122 - (void) mouseDown: (NSEvent *) e
1124 if (! [self doEvent:e type:ButtonPress])
1125 [super mouseDown:e];
1128 - (void) mouseUp: (NSEvent *) e
1130 if (! [self doEvent:e type:ButtonRelease])
1134 - (void) otherMouseDown: (NSEvent *) e
1136 if (! [self doEvent:e type:ButtonPress])
1137 [super otherMouseDown:e];
1140 - (void) otherMouseUp: (NSEvent *) e
1142 if (! [self doEvent:e type:ButtonRelease])
1143 [super otherMouseUp:e];
1146 - (void) mouseMoved: (NSEvent *) e
1148 if (! [self doEvent:e type:MotionNotify])
1149 [super mouseMoved:e];
1152 - (void) mouseDragged: (NSEvent *) e
1154 if (! [self doEvent:e type:MotionNotify])
1155 [super mouseDragged:e];
1158 - (void) otherMouseDragged: (NSEvent *) e
1160 if (! [self doEvent:e type:MotionNotify])
1161 [super otherMouseDragged:e];
1164 - (void) scrollWheel: (NSEvent *) e
1166 if (! [self doEvent:e type:ButtonPress])
1167 [super scrollWheel:e];
1170 - (void) keyDown: (NSEvent *) e
1172 if (! [self doEvent:e type:KeyPress])
1176 - (void) keyUp: (NSEvent *) e
1178 if (! [self doEvent:e type:KeyRelease])
1182 - (void) flagsChanged: (NSEvent *) e
1184 if (! [self doEvent:e type:KeyPress])
1185 [super flagsChanged:e];
1191 /* Called after the device's orientation has changed.
1193 Note: we could include a subclass of UIViewController which
1194 contains a shouldAutorotateToInterfaceOrientation method that
1195 returns YES, in which case Core Animation would auto-rotate our
1196 View for us in response to rotation events... but, that interacts
1197 badly with the EAGLContext -- if you introduce Core Animation into
1198 the path, the OpenGL pipeline probably falls back on software
1199 rendering and performance goes to hell. Also, the scaling and
1200 rotation that Core Animation does interacts incorrectly with the GL
1203 So, we have to hack the rotation animation manually, in the GL world.
1205 Possibly XScreenSaverView should use Core Animation, and
1206 XScreenSaverGLView should override that.
1208 - (void)didRotate:(NSNotification *)notification
1210 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1212 /* If the simulator starts up in the rotated position, sometimes
1213 the UIDevice says we're in Portrait when we're not -- but it
1214 turns out that the UINavigationController knows what's up!
1215 So get it from there.
1217 if (current == UIDeviceOrientationUnknown) {
1218 switch ([[[self window] rootViewController] interfaceOrientation]) {
1219 case UIInterfaceOrientationPortrait:
1220 current = UIDeviceOrientationPortrait;
1222 case UIInterfaceOrientationPortraitUpsideDown:
1223 current = UIDeviceOrientationPortraitUpsideDown;
1225 case UIInterfaceOrientationLandscapeLeft: // It's opposite day
1226 current = UIDeviceOrientationLandscapeRight;
1228 case UIInterfaceOrientationLandscapeRight:
1229 current = UIDeviceOrientationLandscapeLeft;
1236 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1237 an orientation change event with an unknown orientation. Those seem
1238 to always be immediately followed by another orientation change with
1239 a *real* orientation change, so let's try just ignoring those bogus
1240 ones and hoping that the real one comes in shortly...
1242 if (current == UIDeviceOrientationUnknown)
1245 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1246 if (orientation == current) return; // no change
1248 new_orientation = current; // current animation target
1249 rotation_ratio = 0; // start animating
1250 rot_start_time = double_time();
1252 switch (orientation) {
1253 case UIInterfaceOrientationLandscapeRight: angle_from = 90; break;
1254 case UIInterfaceOrientationLandscapeLeft: angle_from = 270; break;
1255 case UIInterfaceOrientationPortraitUpsideDown: angle_from = 180; break;
1256 default: angle_from = 0; break;
1259 switch (new_orientation) {
1260 case UIInterfaceOrientationLandscapeRight: angle_to = 90; break;
1261 case UIInterfaceOrientationLandscapeLeft: angle_to = 270; break;
1262 case UIInterfaceOrientationPortraitUpsideDown: angle_to = 180; break;
1263 default: angle_to = 0; break;
1266 NSRect ff = [self frame];
1268 switch (orientation) {
1269 case UIInterfaceOrientationLandscapeLeft: // from landscape
1270 case UIInterfaceOrientationLandscapeRight:
1271 rot_from.width = ff.size.height;
1272 rot_from.height = ff.size.width;
1274 default: // from portrait
1275 rot_from.width = ff.size.width;
1276 rot_from.height = ff.size.height;
1280 switch (new_orientation) {
1281 case UIInterfaceOrientationLandscapeLeft: // to landscape
1282 case UIInterfaceOrientationLandscapeRight:
1283 rot_to.width = ff.size.height;
1284 rot_to.height = ff.size.width;
1286 default: // to portrait
1287 rot_to.width = ff.size.width;
1288 rot_to.height = ff.size.height;
1293 // If we've done a rotation but the saver hasn't been initialized yet,
1294 // don't bother going through an X11 resize, but just do it now.
1295 rot_start_time = 0; // dawn of time
1296 [self hackRotation];
1301 /* In the simulator, multi-touch sequences look like this:
1303 touchesBegan [touchA, touchB]
1304 touchesEnd [touchA, touchB]
1306 But on real devices, sometimes you get that, but sometimes you get:
1308 touchesBegan [touchA, touchB]
1314 touchesBegan [touchA]
1315 touchesBegan [touchB]
1319 So the only way to properly detect a "pinch" gesture is to remember
1320 the start-point of each touch as it comes in; and the end-point of
1321 each touch as those come in; and only process the gesture once the
1322 number of touchEnds matches the number of touchBegins.
1326 rotate_mouse (int *x, int *y, int w, int h, int rot)
1328 int ox = *x, oy = *y;
1329 if (rot > 45 && rot < 135) { *x = oy; *y = w-ox; }
1330 else if (rot < -45 && rot > -135) { *x = h-oy; *y = ox; }
1331 else if (rot > 135 || rot < -135) { *x = w-ox; *y = h-oy; }
1335 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1337 if (xsft->event_cb && xwindow) {
1338 double s = self.contentScaleFactor;
1340 memset (&xe, 0, sizeof(xe));
1342 int w = s * [self frame].size.width;
1343 int h = s * [self frame].size.height;
1344 for (UITouch *touch in touches) {
1345 CGPoint p = [touch locationInView:self];
1346 xe.xany.type = ButtonPress;
1347 xe.xbutton.button = i + 1;
1348 xe.xbutton.button = i + 1;
1349 xe.xbutton.x = s * p.x;
1350 xe.xbutton.y = s * p.y;
1351 rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
1352 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1353 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1360 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1363 // Double-tap means "exit" and return to selection menu.
1365 for (UITouch *touch in touches) {
1366 if ([touch tapCount] >= 2) {
1367 if ([self isAnimating])
1368 [self stopAnimation];
1369 [self removeFromSuperview];
1374 if (xsft->event_cb && xwindow) {
1375 double s = self.contentScaleFactor;
1377 memset (&xe, 0, sizeof(xe));
1379 int w = s * [self frame].size.width;
1380 int h = s * [self frame].size.height;
1381 for (UITouch *touch in touches) {
1382 CGPoint p = [touch locationInView:self];
1383 xe.xany.type = ButtonRelease;
1384 xe.xbutton.button = i + 1;
1385 xe.xbutton.x = s * p.x;
1386 xe.xbutton.y = s * p.y;
1387 rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
1388 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1389 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1396 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1398 if (xsft->event_cb && xwindow) {
1399 double s = self.contentScaleFactor;
1401 memset (&xe, 0, sizeof(xe));
1403 int w = s * [self frame].size.width;
1404 int h = s * [self frame].size.height;
1405 for (UITouch *touch in touches) {
1406 CGPoint p = [touch locationInView:self];
1407 xe.xany.type = MotionNotify;
1408 xe.xmotion.x = s * p.x;
1409 xe.xmotion.y = s * p.y;
1410 rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
1411 jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1412 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1419 /* We need this to respond to "shake" gestures
1421 - (BOOL)canBecomeFirstResponder {
1426 - (void)setScreenLocked:(BOOL)locked
1428 if (screenLocked == locked) return;
1429 screenLocked = locked;
1431 if ([self isAnimating])
1432 [self stopAnimation];
1434 if (! [self isAnimating])
1435 [self startAnimation];
1440 #endif // USE_IPHONE
1445 /* Utility functions...
1448 static PrefsReader *
1449 get_prefsReader (Display *dpy)
1451 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1453 return [view prefsReader];
1458 get_string_resource (Display *dpy, char *name, char *class)
1460 return [get_prefsReader(dpy) getStringResource:name];
1464 get_boolean_resource (Display *dpy, char *name, char *class)
1466 return [get_prefsReader(dpy) getBooleanResource:name];
1470 get_integer_resource (Display *dpy, char *name, char *class)
1472 return [get_prefsReader(dpy) getIntegerResource:name];
1476 get_float_resource (Display *dpy, char *name, char *class)
1478 return [get_prefsReader(dpy) getFloatResource:name];