1 /* xscreensaver, Copyright (c) 2006-2013 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);
100 if (!obj && defaults)
101 obj = [defaults objectForKey:key];
105 - (void)setObject:(id)value forKey:(NSString *)key
107 if (value && defaults) {
108 // If the value is the default, then remove it instead.
109 NSObject *def = [defaults objectForKey:key];
110 if (def && [def isEqual:value])
113 CFPreferencesSetAppValue ((CFStringRef) key,
114 (CFPropertyListRef) value,
115 (CFStringRef) domain);
121 return CFPreferencesAppSynchronize ((CFStringRef) domain);
125 // Make sure these all call our objectForKey.
126 // Might not be necessary, but safe.
128 - (NSString *)stringForKey:(NSString *)key
130 return [[self objectForKey:key] stringValue];
133 - (NSArray *)arrayForKey:(NSString *)key
135 return (NSArray *) [self objectForKey:key];
138 - (NSDictionary *)dictionaryForKey:(NSString *)key
140 return (NSDictionary *) [self objectForKey:key];
143 - (NSData *)dataForKey:(NSString *)key
145 return (NSData *) [self objectForKey:key];
148 - (NSArray *)stringArrayForKey:(NSString *)key
150 return (NSArray *) [self objectForKey:key];
153 - (NSInteger)integerForKey:(NSString *)key
155 return [[self objectForKey:key] integerValue];
158 - (float)floatForKey:(NSString *)key
160 return [[self objectForKey:key] floatValue];
163 - (double)doubleForKey:(NSString *)key
165 return [[self objectForKey:key] doubleValue];
168 - (BOOL)boolForKey:(NSString *)key
170 return [[self objectForKey:key] integerValue];
173 // Make sure these all call our setObject.
174 // Might not be necessary, but safe.
176 - (void)removeObjectForKey:(NSString *)key
178 [self setObject:NULL forKey:key];
181 - (void)setInteger:(NSInteger)value forKey:(NSString *)key
183 [self setObject:[NSNumber numberWithInteger:value] forKey:key];
186 - (void)setFloat:(float)value forKey:(NSString *)key
188 [self setObject:[NSNumber numberWithFloat:value] forKey:key];
191 - (void)setDouble:(double)value forKey:(NSString *)key
193 [self setObject:[NSNumber numberWithDouble:value] forKey:key];
196 - (void)setBool:(BOOL)value forKey:(NSString *)key
198 [self setObject:[NSNumber numberWithBool:value] forKey:key];
203 #endif // !USE_IPHONE
206 @implementation PrefsReader
208 /* Normally we read resources by looking up "KEY" in the database
209 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
210 app, everything is stored in the database "org.jwz.xscreensaver"
211 instead, so transform keys to "SAVERNAME.KEY".
213 NOTE: This is duplicated in XScreenSaverConfigSheet.m, cause I suck.
215 - (NSString *) makeKey:(NSString *)key
218 NSString *prefix = [saver_name stringByAppendingString:@"."];
219 if (! [key hasPrefix:prefix]) // Don't double up!
220 key = [prefix stringByAppendingString:key];
225 - (NSString *) makeCKey:(const char *)key
227 return [self makeKey:[NSString stringWithCString:key
228 encoding:NSUTF8StringEncoding]];
232 /* Converts an array of "key:value" strings to an NSDictionary.
234 - (NSDictionary *) defaultsToDict: (const char * const *) defs
236 NSDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:20];
238 char *line = strdup (*defs);
241 while (*key == '.' || *key == '*' || *key == ' ' || *key == '\t')
244 while (*val && *val != ':')
246 if (*val != ':') abort();
248 while (*val == ' ' || *val == '\t')
251 unsigned long L = strlen(val);
252 while (L > 0 && (val[L-1] == ' ' || val[L-1] == '\t'))
255 // When storing into preferences, look at the default string and
256 // decide whether it's a boolean, int, float, or string, and store
257 // an object of the appropriate type in the prefs.
259 NSString *nskey = [self makeCKey:key];
264 if (!strcasecmp (val, "true") || !strcasecmp (val, "yes"))
265 nsval = [NSNumber numberWithBool:YES];
266 else if (!strcasecmp (val, "false") || !strcasecmp (val, "no"))
267 nsval = [NSNumber numberWithBool:NO];
268 else if (1 == sscanf (val, " %d %c", &dd, &cc))
269 nsval = [NSNumber numberWithInt:dd];
270 else if (1 == sscanf (val, " %lf %c", &ff, &cc))
271 nsval = [NSNumber numberWithDouble:ff];
273 nsval = [NSString stringWithCString:val encoding:NSUTF8StringEncoding];
275 // NSLog (@"default: \"%@\" = \"%@\" [%@]", nskey, nsval, [nsval class]);
276 [dict setValue:nsval forKey:nskey];
284 /* Initialize the Cocoa preferences database:
285 - sets the default preferences values from the 'defaults' array;
286 - binds 'self' to each preference as an observer;
287 - ensures that nothing is mentioned in 'options' and not in 'defaults';
288 - ensures that nothing is mentioned in 'defaults' and not in 'options'.
290 - (void) registerXrmKeys: (const XrmOptionDescRec *) opts
291 defaults: (const char * const *) defs
293 // Store the contents of 'defaults' into the real preferences database.
294 NSDictionary *defsdict = [self defaultsToDict:defs];
295 [userDefaults registerDefaults:defsdict];
296 [globalDefaults registerDefaults:UPDATER_DEFAULTS];
298 // Save a copy of the default options, since iOS doesn't have
299 // [userDefaultsController initialValues].
302 [defaultOptions release];
303 defaultOptions = [[NSMutableDictionary dictionaryWithCapacity:20]
305 for (NSString *key in defsdict) {
306 [defaultOptions setValue:[defsdict objectForKey:key] forKey:key];
310 userDefaultsController =
311 [[NSUserDefaultsController alloc] initWithDefaults:userDefaults
312 initialValues:defsdict];
313 globalDefaultsController =
314 [[NSUserDefaultsController alloc] initWithDefaults:globalDefaults
315 initialValues:defsdict];
317 userDefaultsController = [userDefaults retain];
318 globalDefaultsController = [userDefaults retain];
319 # endif // USE_IPHONE
321 NSDictionary *optsdict = [NSMutableDictionary dictionaryWithCapacity:20];
323 while (opts[0].option) {
324 //const char *option = opts->option;
325 const char *resource = opts->specifier;
327 while (*resource == '.' || *resource == '*')
329 NSString *nsresource = [self makeCKey:resource];
331 // make sure there's no resource mentioned in options and not defaults.
332 if (![defsdict objectForKey:nsresource]) {
333 if (! (!strcmp(resource, "font") || // don't warn about these
334 !strcmp(resource, "foreground") ||
335 !strcmp(resource, "textLiteral") ||
336 !strcmp(resource, "textFile") ||
337 !strcmp(resource, "textURL") ||
338 !strcmp(resource, "textProgram") ||
339 !strcmp(resource, "imageDirectory")))
340 NSLog (@"warning: \"%s\" is in options but not defaults", resource);
342 [optsdict setValue:nsresource forKey:nsresource];
348 // make sure there's no resource mentioned in defaults and not options.
349 for (NSString *key in defsdict) {
350 if (! [optsdict objectForKey:key])
351 if (! ([key isEqualToString:@"foreground"] || // don't warn about these
352 [key isEqualToString:@"background"] ||
353 [key isEqualToString:@"Background"] ||
354 [key isEqualToString:@"geometry"] ||
355 [key isEqualToString:@"font"] ||
356 [key isEqualToString:@"dontClearRoot"] ||
359 [key isEqualToString:@"fpsSolid"] ||
360 [key isEqualToString:@"fpsTop"] ||
361 [key isEqualToString:@"titleFont"] ||
363 // analogtv.c settings
364 [key isEqualToString:@"TVBrightness"] ||
365 [key isEqualToString:@"TVColor"] ||
366 [key isEqualToString:@"TVContrast"] ||
367 [key isEqualToString:@"TVTint"]
369 NSLog (@"warning: \"%@\" is in defaults but not options", key);
374 // Dump the entire resource database.
375 NSLog(@"userDefaults:");
376 NSDictionary *d = [userDefaults dictionaryRepresentation];
377 for (NSObject *key in [[d allKeys]
378 sortedArrayUsingSelector:@selector(compare:)]) {
379 NSObject *val = [d objectForKey:key];
380 NSLog (@"%@ = %@", key, val);
382 NSLog(@"globalDefaults:");
383 d = [globalDefaults dictionaryRepresentation];
384 for (NSObject *key in [[d allKeys]
385 sortedArrayUsingSelector:@selector(compare:)]) {
386 NSObject *val = [d objectForKey:key];
387 NSLog (@"%@ = %@", key, val);
393 - (NSUserDefaultsController *) userDefaultsController
395 NSAssert(userDefaultsController, @"userDefaultsController uninitialized");
396 return userDefaultsController;
399 - (NSUserDefaultsController *) globalDefaultsController
401 NSAssert(globalDefaultsController, @"globalDefaultsController uninitialized");
402 return globalDefaultsController;
405 - (NSDictionary *) defaultOptions
407 NSAssert(defaultOptions, @"defaultOptions uninitialized");
408 return defaultOptions;
412 - (NSObject *) getObjectResource: (const char *) name
414 // First look in userDefaults, then in globalDefaults.
415 for (int globalp = 0; globalp <= 1; globalp++) {
416 const char *name2 = name;
418 NSString *key = [self makeCKey:name2];
419 NSObject *obj = [(globalp ? globalDefaults : userDefaults)
424 // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz".
426 const char *dot = strchr (name2, '.');
437 - (char *) getStringResource: (const char *) name
439 NSObject *o = [self getObjectResource:name];
440 //NSLog(@"%s = %@",name,o);
442 if (! (!strcmp(name, "eraseMode") || // erase.c
443 // xlockmore.c reads all of these whether used or not...
444 !strcmp(name, "right3d") ||
445 !strcmp(name, "left3d") ||
446 !strcmp(name, "both3d") ||
447 !strcmp(name, "none3d") ||
448 !strcmp(name, "font") ||
449 !strcmp(name, "labelFont") || // grabclient.c
450 !strcmp(name, "titleFont") ||
451 !strcmp(name, "fpsFont") || // fps.c
452 !strcmp(name, "foreground") || // fps.c
453 !strcmp(name, "background") ||
454 !strcmp(name, "textLiteral")
456 NSLog(@"warning: no preference \"%s\" [string]", name);
459 if (! [o isKindOfClass:[NSString class]]) {
460 NSLog(@"asked for %s as a string, but it is a %@", name, [o class]);
461 o = [(NSNumber *) o stringValue];
464 NSString *os = (NSString *) o;
465 char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]);
467 // Kludge: if the string is surrounded with single-quotes, remove them.
468 // This happens when the .xml file says things like arg="-foo 'bar baz'"
469 if (result[0] == '\'' && result[strlen(result)-1] == '\'') {
470 result[strlen(result)-1] = 0;
471 strcpy (result, result+1);
474 // Kludge: assume that any string that begins with "~" and has a "/"
475 // anywhere in it should be expanded as if it is a pathname.
476 if (result[0] == '~' && strchr (result, '/')) {
477 os = [NSString stringWithCString:result encoding:NSUTF8StringEncoding];
479 result = strdup ([[os stringByExpandingTildeInPath]
480 cStringUsingEncoding:NSUTF8StringEncoding]);
487 - (double) getFloatResource: (const char *) name
489 NSObject *o = [self getObjectResource:name];
491 // xlockmore.c reads all of these whether used or not...
492 if (! (!strcmp(name, "cycles") ||
493 !strcmp(name, "size") ||
494 !strcmp(name, "use3d") ||
495 !strcmp(name, "delta3d") ||
496 !strcmp(name, "wireframe") ||
497 !strcmp(name, "showFPS") ||
498 !strcmp(name, "fpsSolid") ||
499 !strcmp(name, "fpsTop") ||
500 !strcmp(name, "mono") ||
501 !strcmp(name, "count") ||
502 !strcmp(name, "ncolors") ||
503 !strcmp(name, "doFPS") || // fps.c
504 !strcmp(name, "eraseSeconds") // erase.c
506 NSLog(@"warning: no preference \"%s\" [float]", name);
509 if ([o isKindOfClass:[NSString class]]) {
510 return [(NSString *) o doubleValue];
511 } else if ([o isKindOfClass:[NSNumber class]]) {
512 return [(NSNumber *) o doubleValue];
514 NSAssert2(0, @"%s = \"%@\" but should have been an NSNumber", name, o);
520 - (int) getIntegerResource: (const char *) name
522 // Sliders might store float values for integral resources; round them.
523 float v = [self getFloatResource:name];
524 int i = (int) (v + (v < 0 ? -0.5 : 0.5)); // ignore sign or -1 rounds to 0
525 // if (i != v) NSLog(@"%s: rounded %.3f to %d", name, v, i);
530 - (BOOL) getBooleanResource: (const char *) name
532 NSObject *o = [self getObjectResource:name];
535 } else if ([o isKindOfClass:[NSNumber class]]) {
536 double n = [(NSNumber *) o doubleValue];
537 if (n == 0) return NO;
538 else if (n == 1) return YES;
540 } else if ([o isKindOfClass:[NSString class]]) {
541 NSString *s = [((NSString *) o) lowercaseString];
542 if ([s isEqualToString:@"true"] ||
543 [s isEqualToString:@"yes"] ||
544 [s isEqualToString:@"1"])
546 else if ([s isEqualToString:@"false"] ||
547 [s isEqualToString:@"no"] ||
548 [s isEqualToString:@"0"] ||
549 [s isEqualToString:@""])
555 NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o);
561 - (id) initWithName: (NSString *) name
562 xrmKeys: (const XrmOptionDescRec *) opts
563 defaults: (const char * const *) defs
566 if (!self) return nil;
569 userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name];
570 globalDefaults = [[GlobalDefaults alloc] initWithDomain:@UPDATER_DOMAIN
573 userDefaults = [NSUserDefaults standardUserDefaults];
574 globalDefaults = [userDefaults retain];
575 # endif // USE_IPHONE
577 // Convert "org.jwz.xscreensaver.NAME" to just "NAME".
578 NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch];
580 name = [name substringFromIndex:r.location+1];
581 name = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
582 saver_name = [name retain];
584 [self registerXrmKeys:opts defaults:defs];
590 [saver_name release];
591 [userDefaultsController release];
592 [globalDefaultsController release];
593 [globalDefaults release];