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:UPDATER_DEFAULTS];
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 // Only look in globalDefaults for updater preferences.
416 static NSDictionary *updaterDefaults;
417 if (!updaterDefaults) {
418 updaterDefaults = UPDATER_DEFAULTS;
419 [updaterDefaults retain];
422 NSUserDefaults *defaults =
423 [updaterDefaults objectForKey:[NSString stringWithUTF8String:name]] ?
427 const char *name2 = name;
429 NSString *key = [self makeCKey:name2];
430 NSObject *obj = [defaults objectForKey:key];
434 // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz".
436 const char *dot = strchr (name2, '.');
446 - (char *) getStringResource: (const char *) name
448 NSObject *o = [self getObjectResource:name];
449 //NSLog(@"%s = %@",name,o);
451 if (! (!strcmp(name, "eraseMode") || // erase.c
452 // xlockmore.c reads all of these whether used or not...
453 !strcmp(name, "right3d") ||
454 !strcmp(name, "left3d") ||
455 !strcmp(name, "both3d") ||
456 !strcmp(name, "none3d") ||
457 !strcmp(name, "font") ||
458 !strcmp(name, "labelFont") || // grabclient.c
459 !strcmp(name, "titleFont") ||
460 !strcmp(name, "fpsFont") || // fps.c
461 !strcmp(name, "foreground") || // fps.c
462 !strcmp(name, "background") ||
463 !strcmp(name, "textLiteral")
465 NSLog(@"warning: no preference \"%s\" [string]", name);
468 if (! [o isKindOfClass:[NSString class]]) {
469 NSLog(@"asked for %s as a string, but it is a %@", name, [o class]);
470 o = [(NSNumber *) o stringValue];
473 NSString *os = (NSString *) o;
474 char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]);
476 // Kludge: if the string is surrounded with single-quotes, remove them.
477 // This happens when the .xml file says things like arg="-foo 'bar baz'"
478 if (result[0] == '\'' && result[strlen(result)-1] == '\'') {
479 result[strlen(result)-1] = 0;
480 strcpy (result, result+1);
483 // Kludge: assume that any string that begins with "~" and has a "/"
484 // anywhere in it should be expanded as if it is a pathname.
485 if (result[0] == '~' && strchr (result, '/')) {
486 os = [NSString stringWithCString:result encoding:NSUTF8StringEncoding];
488 result = strdup ([[os stringByExpandingTildeInPath]
489 cStringUsingEncoding:NSUTF8StringEncoding]);
496 - (double) getFloatResource: (const char *) name
498 NSObject *o = [self getObjectResource:name];
500 // xlockmore.c reads all of these whether used or not...
501 if (! (!strcmp(name, "cycles") ||
502 !strcmp(name, "size") ||
503 !strcmp(name, "use3d") ||
504 !strcmp(name, "delta3d") ||
505 !strcmp(name, "wireframe") ||
506 !strcmp(name, "showFPS") ||
507 !strcmp(name, "fpsSolid") ||
508 !strcmp(name, "fpsTop") ||
509 !strcmp(name, "mono") ||
510 !strcmp(name, "count") ||
511 !strcmp(name, "ncolors") ||
512 !strcmp(name, "doFPS") || // fps.c
513 !strcmp(name, "eraseSeconds") // erase.c
515 NSLog(@"warning: no preference \"%s\" [float]", name);
518 if ([o isKindOfClass:[NSString class]]) {
519 return [(NSString *) o doubleValue];
520 } else if ([o isKindOfClass:[NSNumber class]]) {
521 return [(NSNumber *) o doubleValue];
523 NSAssert2(0, @"%s = \"%@\" but should have been an NSNumber", name, o);
529 - (int) getIntegerResource: (const char *) name
531 // Sliders might store float values for integral resources; round them.
532 float v = [self getFloatResource:name];
533 int i = (int) (v + (v < 0 ? -0.5 : 0.5)); // ignore sign or -1 rounds to 0
534 // if (i != v) NSLog(@"%s: rounded %.3f to %d", name, v, i);
539 - (BOOL) getBooleanResource: (const char *) name
541 NSObject *o = [self getObjectResource:name];
544 } else if ([o isKindOfClass:[NSNumber class]]) {
545 double n = [(NSNumber *) o doubleValue];
546 if (n == 0) return NO;
547 else if (n == 1) return YES;
549 } else if ([o isKindOfClass:[NSString class]]) {
550 NSString *s = [((NSString *) o) lowercaseString];
551 if ([s isEqualToString:@"true"] ||
552 [s isEqualToString:@"yes"] ||
553 [s isEqualToString:@"1"])
555 else if ([s isEqualToString:@"false"] ||
556 [s isEqualToString:@"no"] ||
557 [s isEqualToString:@"0"] ||
558 [s isEqualToString:@""])
564 NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o);
570 - (id) initWithName: (NSString *) name
571 xrmKeys: (const XrmOptionDescRec *) opts
572 defaults: (const char * const *) defs
575 if (!self) return nil;
578 userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name];
579 globalDefaults = [[GlobalDefaults alloc] initWithDomain:@UPDATER_DOMAIN
582 userDefaults = [NSUserDefaults standardUserDefaults];
583 globalDefaults = [userDefaults retain];
584 # endif // USE_IPHONE
586 // Convert "org.jwz.xscreensaver.NAME" to just "NAME".
587 NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch];
589 name = [name substringFromIndex:r.location+1];
590 name = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
591 saver_name = [name retain];
593 [self registerXrmKeys:opts defaults:defs];
599 [saver_name release];
600 [userDefaultsController release];
601 [globalDefaultsController release];
602 [globalDefaults release];