1 /* xscreensaver, Copyright (c) 2006-2015 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 implements the substrate of the xscreensaver preferences code:
13 It does this by writing defaults to, and reading values from, the
14 NSUserDefaultsController (and ScreenSaverDefaults/NSUserDefaults)
15 and thereby reading the preferences that may have been edited by
16 the UI (XScreenSaverConfigSheet).
20 # import <ScreenSaver/ScreenSaverDefaults.h>
23 #import "PrefsReader.h"
25 #import "screenhackI.h"
29 #include <objc/runtime.h>
31 /* GlobalDefaults is an NSUserDefaults implementation that writes into
32 the preferences key we provide, instead of whatever the default would
33 be for this app. We do this by invoking the Core Foundation preferences
34 routines directly, while presenting the same API as NSUserDefaults.
36 We need this so that global prefs will go into the file
37 Library/Preferences/org.jwz.xscreensaver.updater.plist instead of into
38 Library/Preferences/ByHost/org.jwz.xscreensaver.Maze.XXXXX.plist
39 with the per-saver prefs.
41 The ScreenSaverDefaults class *almost* does this, but it always writes
42 into the ByHost subdirectory, which means it's not readable by an app
43 that tries to access it with a plain old +standardUserDefaults.
45 @interface GlobalDefaults : NSUserDefaults
48 NSDictionary *defaults;
52 @implementation GlobalDefaults
53 - (id) initWithDomain:(NSString *)_domain module:(NSString *)_module
55 // Key-Value Observing tries to create an Objective-C class named
56 // NSKVONotifying_GlobalDefaults when the configuration page is shown. But if
57 // this is the second XScreenSaver .saver running in the same process, class
58 // creation fails because that class name was already used by the first
59 // .saver, and it refers to the GlobalDefaults from the other .saver.
61 // This gives the class a unique name, sidestepping the above issue.
63 // It really just needs to be unique for this .saver and this instance.
64 // Using the pointer to the .saver's mach_header and the full path to the
65 // .saver would be preferable, but this should be good enough.
67 sprintf(class_name, "GlobalDefaults_%s_%p_%u",
68 strrchr(_module.UTF8String, '.') + 1, self, random());
69 Class c = objc_allocateClassPair([GlobalDefaults class], class_name, 0);
72 objc_registerClassPair(c);
75 object_setClass(self, c);
76 domain = [_domain retain];
82 Class c = object_getClass(self);
88 objc_disposeClassPair(c);
91 - (void)registerDefaults:(NSDictionary *)dict
93 defaults = [dict retain];
96 - (id)objectForKey:(NSString *)key
98 NSObject *obj = (NSObject *)
99 CFPreferencesCopyAppValue ((CFStringRef) key, (CFStringRef) domain);
103 obj = [defaults objectForKey:key];
107 - (void)setObject:(id)value forKey:(NSString *)key
109 if (value && defaults) {
110 // If the value is the default, then remove it instead.
111 NSObject *def = [defaults objectForKey:key];
112 if (def && [def isEqual:value])
115 CFPreferencesSetAppValue ((CFStringRef) key,
116 (CFPropertyListRef) value,
117 (CFStringRef) domain);
123 return CFPreferencesAppSynchronize ((CFStringRef) domain);
127 // Make sure these all call our objectForKey.
128 // Might not be necessary, but safe.
130 - (NSString *)stringForKey:(NSString *)key
132 return [[self objectForKey:key] stringValue];
135 - (NSArray *)arrayForKey:(NSString *)key
137 return (NSArray *) [self objectForKey:key];
140 - (NSDictionary *)dictionaryForKey:(NSString *)key
142 return (NSDictionary *) [self objectForKey:key];
145 - (NSData *)dataForKey:(NSString *)key
147 return (NSData *) [self objectForKey:key];
150 - (NSArray *)stringArrayForKey:(NSString *)key
152 return (NSArray *) [self objectForKey:key];
155 - (NSInteger)integerForKey:(NSString *)key
157 return [[self objectForKey:key] integerValue];
160 - (float)floatForKey:(NSString *)key
162 return [[self objectForKey:key] floatValue];
165 - (double)doubleForKey:(NSString *)key
167 return [[self objectForKey:key] doubleValue];
170 - (BOOL)boolForKey:(NSString *)key
172 return [[self objectForKey:key] integerValue];
175 // Make sure these all call our setObject.
176 // Might not be necessary, but safe.
178 - (void)removeObjectForKey:(NSString *)key
180 [self setObject:NULL forKey:key];
183 - (void)setInteger:(NSInteger)value forKey:(NSString *)key
185 [self setObject:[NSNumber numberWithInteger:value] forKey:key];
188 - (void)setFloat:(float)value forKey:(NSString *)key
190 [self setObject:[NSNumber numberWithFloat:value] forKey:key];
193 - (void)setDouble:(double)value forKey:(NSString *)key
195 [self setObject:[NSNumber numberWithDouble:value] forKey:key];
198 - (void)setBool:(BOOL)value forKey:(NSString *)key
200 [self setObject:[NSNumber numberWithBool:value] forKey:key];
205 #endif // !USE_IPHONE
208 @implementation PrefsReader
210 /* Normally we read resources by looking up "KEY" in the database
211 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
212 app, everything is stored in the database "org.jwz.xscreensaver"
213 instead, so transform keys to "SAVERNAME.KEY".
215 NOTE: This is duplicated in XScreenSaverConfigSheet.m, cause I suck.
217 - (NSString *) makeKey:(NSString *)key
220 NSString *prefix = [saver_name stringByAppendingString:@"."];
221 if (! [key hasPrefix:prefix]) // Don't double up!
222 key = [prefix stringByAppendingString:key];
227 - (NSString *) makeCKey:(const char *)key
229 return [self makeKey:[NSString stringWithCString:key
230 encoding:NSUTF8StringEncoding]];
234 /* Converts an array of "key:value" strings to an NSDictionary.
236 - (NSDictionary *) defaultsToDict: (const char * const *) defs
238 NSDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:20];
240 char *line = strdup (*defs);
243 while (*key == '.' || *key == '*' || *key == ' ' || *key == '\t')
246 while (*val && *val != ':')
248 if (*val != ':') abort();
250 while (*val == ' ' || *val == '\t')
253 unsigned long L = strlen(val);
254 while (L > 0 && (val[L-1] == ' ' || val[L-1] == '\t'))
257 // When storing into preferences, look at the default string and
258 // decide whether it's a boolean, int, float, or string, and store
259 // an object of the appropriate type in the prefs.
261 NSString *nskey = [self makeCKey:key];
266 if (!strcasecmp (val, "true") || !strcasecmp (val, "yes"))
267 nsval = [NSNumber numberWithBool:YES];
268 else if (!strcasecmp (val, "false") || !strcasecmp (val, "no"))
269 nsval = [NSNumber numberWithBool:NO];
270 else if (1 == sscanf (val, " %d %c", &dd, &cc))
271 nsval = [NSNumber numberWithInt:dd];
272 else if (1 == sscanf (val, " %lf %c", &ff, &cc))
273 nsval = [NSNumber numberWithDouble:ff];
275 nsval = [NSString stringWithCString:val encoding:NSUTF8StringEncoding];
277 // NSLog (@"default: \"%@\" = \"%@\" [%@]", nskey, nsval, [nsval class]);
278 [dict setValue:nsval forKey:nskey];
286 /* Initialize the Cocoa preferences database:
287 - sets the default preferences values from the 'defaults' array;
288 - binds 'self' to each preference as an observer;
289 - ensures that nothing is mentioned in 'options' and not in 'defaults';
290 - ensures that nothing is mentioned in 'defaults' and not in 'options'.
292 - (void) registerXrmKeys: (const XrmOptionDescRec *) opts
293 defaults: (const char * const *) defs
295 // Store the contents of 'defaults' into the real preferences database.
296 NSDictionary *defsdict = [self defaultsToDict:defs];
297 [userDefaults registerDefaults:defsdict];
298 [globalDefaults registerDefaults:UPDATER_DEFAULTS];
300 // Save a copy of the default options, since iOS doesn't have
301 // [userDefaultsController initialValues].
304 [defaultOptions release];
305 defaultOptions = [[NSMutableDictionary dictionaryWithCapacity:20]
307 for (NSString *key in defsdict) {
308 [defaultOptions setValue:[defsdict objectForKey:key] forKey:key];
312 userDefaultsController =
313 [[NSUserDefaultsController alloc] initWithDefaults:userDefaults
314 initialValues:defsdict];
315 globalDefaultsController =
316 [[NSUserDefaultsController alloc] initWithDefaults:globalDefaults
317 initialValues:UPDATER_DEFAULTS];
319 userDefaultsController = [userDefaults retain];
320 globalDefaultsController = [userDefaults retain];
321 # endif // USE_IPHONE
323 NSDictionary *optsdict = [NSMutableDictionary dictionaryWithCapacity:20];
325 while (opts[0].option) {
326 //const char *option = opts->option;
327 const char *resource = opts->specifier;
329 while (*resource == '.' || *resource == '*')
331 NSString *nsresource = [self makeCKey:resource];
333 // make sure there's no resource mentioned in options and not defaults.
334 if (![defsdict objectForKey:nsresource]) {
335 if (! (!strcmp(resource, "font") || // don't warn about these
336 !strcmp(resource, "foreground") ||
337 !strcmp(resource, "textLiteral") ||
338 !strcmp(resource, "textFile") ||
339 !strcmp(resource, "textURL") ||
340 !strcmp(resource, "textProgram") ||
341 !strcmp(resource, "imageDirectory")))
342 NSLog (@"warning: \"%s\" is in options but not defaults", resource);
344 [optsdict setValue:nsresource forKey:nsresource];
350 // make sure there's no resource mentioned in defaults and not options.
351 for (NSString *key in defsdict) {
352 if (! [optsdict objectForKey:key])
353 if (! ([key isEqualToString:@"foreground"] || // don't warn about these
354 [key isEqualToString:@"background"] ||
355 [key isEqualToString:@"Background"] ||
356 [key isEqualToString:@"geometry"] ||
357 [key isEqualToString:@"font"] ||
358 [key isEqualToString:@"dontClearRoot"] ||
361 [key isEqualToString:@"fpsSolid"] ||
362 [key isEqualToString:@"fpsTop"] ||
363 [key isEqualToString:@"titleFont"] ||
365 // analogtv.c settings
366 [key isEqualToString:@"TVBrightness"] ||
367 [key isEqualToString:@"TVColor"] ||
368 [key isEqualToString:@"TVContrast"] ||
369 [key isEqualToString:@"TVTint"]
371 NSLog (@"warning: \"%@\" is in defaults but not options", key);
376 // Dump the entire resource database.
377 NSLog(@"userDefaults:");
378 NSDictionary *d = [userDefaults dictionaryRepresentation];
379 for (NSObject *key in [[d allKeys]
380 sortedArrayUsingSelector:@selector(compare:)]) {
381 NSObject *val = [d objectForKey:key];
382 NSLog (@"%@ = %@", key, val);
384 NSLog(@"globalDefaults:");
385 d = [globalDefaults dictionaryRepresentation];
386 for (NSObject *key in [[d allKeys]
387 sortedArrayUsingSelector:@selector(compare:)]) {
388 NSObject *val = [d objectForKey:key];
389 NSLog (@"%@ = %@", key, val);
395 - (NSUserDefaultsController *) userDefaultsController
397 NSAssert(userDefaultsController, @"userDefaultsController uninitialized");
398 return userDefaultsController;
401 - (NSUserDefaultsController *) globalDefaultsController
403 NSAssert(globalDefaultsController, @"globalDefaultsController uninitialized");
404 return globalDefaultsController;
407 - (NSDictionary *) defaultOptions
409 NSAssert(defaultOptions, @"defaultOptions uninitialized");
410 return defaultOptions;
414 - (NSObject *) getObjectResource: (const char *) name
416 // Only look in globalDefaults for updater preferences.
418 static NSDictionary *updaterDefaults;
419 if (!updaterDefaults) {
420 updaterDefaults = UPDATER_DEFAULTS;
421 [updaterDefaults retain];
424 NSUserDefaults *defaults =
425 [updaterDefaults objectForKey:[NSString stringWithUTF8String:name]] ?
429 const char *name2 = name;
431 NSString *key = [self makeCKey:name2];
432 NSObject *obj = [defaults objectForKey:key];
436 // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz".
438 const char *dot = strchr (name2, '.');
448 - (char *) getStringResource: (const char *) name
450 NSObject *o = [self getObjectResource:name];
451 //NSLog(@"%s = %@",name,o);
453 if (! (!strcmp(name, "eraseMode") || // erase.c
454 // xlockmore.c reads all of these whether used or not...
455 !strcmp(name, "right3d") ||
456 !strcmp(name, "left3d") ||
457 !strcmp(name, "both3d") ||
458 !strcmp(name, "none3d") ||
459 !strcmp(name, "font") ||
460 !strcmp(name, "labelFont") || // grabclient.c
461 !strcmp(name, "titleFont") ||
462 !strcmp(name, "fpsFont") || // fps.c
463 !strcmp(name, "foreground") || // fps.c
464 !strcmp(name, "background") ||
465 !strcmp(name, "textLiteral")
467 NSLog(@"warning: no preference \"%s\" [string]", name);
470 if (! [o isKindOfClass:[NSString class]]) {
471 NSLog(@"asked for %s as a string, but it is a %@", name, [o class]);
472 o = [(NSNumber *) o stringValue];
475 NSString *os = (NSString *) o;
476 char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]);
478 // Kludge: if the string is surrounded with single-quotes, remove them.
479 // This happens when the .xml file says things like arg="-foo 'bar baz'"
480 if (result[0] == '\'' && result[strlen(result)-1] == '\'') {
481 result[strlen(result)-1] = 0;
482 strcpy (result, result+1);
485 // Kludge: assume that any string that begins with "~" and has a "/"
486 // anywhere in it should be expanded as if it is a pathname.
487 if (result[0] == '~' && strchr (result, '/')) {
488 os = [NSString stringWithCString:result encoding:NSUTF8StringEncoding];
490 result = strdup ([[os stringByExpandingTildeInPath]
491 cStringUsingEncoding:NSUTF8StringEncoding]);
498 - (double) getFloatResource: (const char *) name
500 NSObject *o = [self getObjectResource:name];
502 // xlockmore.c reads all of these whether used or not...
503 if (! (!strcmp(name, "cycles") ||
504 !strcmp(name, "size") ||
505 !strcmp(name, "use3d") ||
506 !strcmp(name, "delta3d") ||
507 !strcmp(name, "wireframe") ||
508 !strcmp(name, "showFPS") ||
509 !strcmp(name, "fpsSolid") ||
510 !strcmp(name, "fpsTop") ||
511 !strcmp(name, "mono") ||
512 !strcmp(name, "count") ||
513 !strcmp(name, "ncolors") ||
514 !strcmp(name, "doFPS") || // fps.c
515 !strcmp(name, "eraseSeconds") // erase.c
517 NSLog(@"warning: no preference \"%s\" [float]", name);
520 if ([o isKindOfClass:[NSString class]]) {
521 return [(NSString *) o doubleValue];
522 } else if ([o isKindOfClass:[NSNumber class]]) {
523 return [(NSNumber *) o doubleValue];
525 NSAssert2(0, @"%s = \"%@\" but should have been an NSNumber", name, o);
531 - (int) getIntegerResource: (const char *) name
533 // Sliders might store float values for integral resources; round them.
534 float v = [self getFloatResource:name];
535 int i = (int) (v + (v < 0 ? -0.5 : 0.5)); // ignore sign or -1 rounds to 0
536 // if (i != v) NSLog(@"%s: rounded %.3f to %d", name, v, i);
541 - (BOOL) getBooleanResource: (const char *) name
543 NSObject *o = [self getObjectResource:name];
546 } else if ([o isKindOfClass:[NSNumber class]]) {
547 double n = [(NSNumber *) o doubleValue];
548 if (n == 0) return NO;
549 else if (n == 1) return YES;
551 } else if ([o isKindOfClass:[NSString class]]) {
552 NSString *s = [((NSString *) o) lowercaseString];
553 if ([s isEqualToString:@"true"] ||
554 [s isEqualToString:@"yes"] ||
555 [s isEqualToString:@"1"])
557 else if ([s isEqualToString:@"false"] ||
558 [s isEqualToString:@"no"] ||
559 [s isEqualToString:@"0"] ||
560 [s isEqualToString:@""])
566 NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o);
572 - (id) initWithName: (NSString *) name
573 xrmKeys: (const XrmOptionDescRec *) opts
574 defaults: (const char * const *) defs
577 if (!self) return nil;
580 userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name];
581 globalDefaults = [[GlobalDefaults alloc] initWithDomain:@UPDATER_DOMAIN
584 userDefaults = [NSUserDefaults standardUserDefaults];
585 globalDefaults = [userDefaults retain];
586 # endif // USE_IPHONE
588 // Convert "org.jwz.xscreensaver.NAME" to just "NAME".
589 NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch];
591 name = [name substringFromIndex:r.location+1];
592 name = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
593 saver_name = [name retain];
595 [self registerXrmKeys:opts defaults:defs];
601 [saver_name release];
602 [userDefaultsController release];
603 [globalDefaultsController release];
604 [globalDefaults release];