X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?p=xscreensaver;a=blobdiff_plain;f=OSX%2FPrefsReader.m;h=cf1698ece73a964928f56bc7510617fa2f89bf6b;hp=9ff55bba23692d2bad162209d6632cd80991dab3;hb=019de959b265701cd0c3fccbb61f2b69f06bf9ee;hpb=c494fd2e6b3b25582375d62e40f4f5cc984ca424 diff --git a/OSX/PrefsReader.m b/OSX/PrefsReader.m index 9ff55bba..cf1698ec 100644 --- a/OSX/PrefsReader.m +++ b/OSX/PrefsReader.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006 Jamie Zawinski +/* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that @@ -16,12 +16,194 @@ the UI (XScreenSaverConfigSheet). */ -#import +#ifndef USE_IPHONE +# import +#endif + #import "PrefsReader.h" +#import "Updater.h" #import "screenhackI.h" +#ifndef USE_IPHONE + + +/* GlobalDefaults is an NSUserDefaults implementation that writes into + the preferences key we provide, instead of whatever the default would + be for this app. We do this by invoking the Core Foundation preferences + routines directly, while presenting the same API as NSUserDefaults. + + We need this so that global prefs will go into the file + Library/Preferences/org.jwz.xscreensaver.updater.plist instead of into + Library/Preferences/ByHost/org.jwz.xscreensaver.Maze.XXXXX.plist + with the per-saver prefs. + + The ScreenSaverDefaults class *almost* does this, but it always writes + into the ByHost subdirectory, which means it's not readable by an app + that tries to access it with a plain old +standardUserDefaults. + */ +@interface GlobalDefaults : NSUserDefaults +{ + NSString *domain; + NSDictionary *defaults; +} +@end + +@implementation GlobalDefaults +- (id) initWithDomain:(NSString *)_domain +{ + self = [super init]; + domain = [_domain retain]; + return self; +} + +- (void) dealloc +{ + [domain release]; + [defaults release]; + [super dealloc]; +} + +- (void)registerDefaults:(NSDictionary *)dict +{ + defaults = [dict retain]; +} + +- (id)objectForKey:(NSString *)key +{ + NSObject *obj = (NSObject *) + CFPreferencesCopyAppValue ((CFStringRef) key, (CFStringRef) domain); + if (!obj && defaults) + obj = [defaults objectForKey:key]; + return obj; +} + +- (void)setObject:(id)value forKey:(NSString *)key +{ + if (value && defaults) { + // If the value is the default, then remove it instead. + NSObject *def = [defaults objectForKey:key]; + if (def && [def isEqual:value]) + value = NULL; + } + CFPreferencesSetAppValue ((CFStringRef) key, + (CFPropertyListRef) value, + (CFStringRef) domain); +} + + +- (BOOL)synchronize +{ + return CFPreferencesAppSynchronize ((CFStringRef) domain); +} + + +// Make sure these all call our objectForKey. +// Might not be necessary, but safe. + +- (NSString *)stringForKey:(NSString *)key +{ + return [[self objectForKey:key] stringValue]; +} + +- (NSArray *)arrayForKey:(NSString *)key +{ + return (NSArray *) [self objectForKey:key]; +} + +- (NSDictionary *)dictionaryForKey:(NSString *)key +{ + return (NSDictionary *) [self objectForKey:key]; +} + +- (NSData *)dataForKey:(NSString *)key +{ + return (NSData *) [self objectForKey:key]; +} + +- (NSArray *)stringArrayForKey:(NSString *)key +{ + return (NSArray *) [self objectForKey:key]; +} + +- (NSInteger)integerForKey:(NSString *)key +{ + return [[self objectForKey:key] integerValue]; +} + +- (float)floatForKey:(NSString *)key +{ + return [[self objectForKey:key] floatValue]; +} + +- (double)doubleForKey:(NSString *)key +{ + return [[self objectForKey:key] doubleValue]; +} + +- (BOOL)boolForKey:(NSString *)key +{ + return [[self objectForKey:key] integerValue]; +} + +// Make sure these all call our setObject. +// Might not be necessary, but safe. + +- (void)removeObjectForKey:(NSString *)key +{ + [self setObject:NULL forKey:key]; +} + +- (void)setInteger:(NSInteger)value forKey:(NSString *)key +{ + [self setObject:[NSNumber numberWithInteger:value] forKey:key]; +} + +- (void)setFloat:(float)value forKey:(NSString *)key +{ + [self setObject:[NSNumber numberWithFloat:value] forKey:key]; +} + +- (void)setDouble:(double)value forKey:(NSString *)key +{ + [self setObject:[NSNumber numberWithDouble:value] forKey:key]; +} + +- (void)setBool:(BOOL)value forKey:(NSString *)key +{ + [self setObject:[NSNumber numberWithBool:value] forKey:key]; +} +@end + + +#endif // !USE_IPHONE + + @implementation PrefsReader +/* Normally we read resources by looking up "KEY" in the database + "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone + app, everything is stored in the database "org.jwz.xscreensaver" + instead, so transform keys to "SAVERNAME.KEY". + + NOTE: This is duplicated in XScreenSaverConfigSheet.m, cause I suck. + */ +- (NSString *) makeKey:(NSString *)key +{ +# ifdef USE_IPHONE + NSString *prefix = [saver_name stringByAppendingString:@"."]; + if (! [key hasPrefix:prefix]) // Don't double up! + key = [prefix stringByAppendingString:key]; +# endif + return key; +} + +- (NSString *) makeCKey:(const char *)key +{ + return [self makeKey:[NSString stringWithCString:key + encoding:NSUTF8StringEncoding]]; +} + + /* Converts an array of "key:value" strings to an NSDictionary. */ - (NSDictionary *) defaultsToDict: (const char * const *) defs @@ -49,8 +231,7 @@ // decide whether it's a boolean, int, float, or string, and store // an object of the appropriate type in the prefs. // - NSString *nskey = [NSString stringWithCString:key - encoding:NSUTF8StringEncoding]; + NSString *nskey = [self makeCKey:key]; NSObject *nsval; int dd; double ff; @@ -87,10 +268,30 @@ // Store the contents of 'defaults' into the real preferences database. NSDictionary *defsdict = [self defaultsToDict:defs]; [userDefaults registerDefaults:defsdict]; + [globalDefaults registerDefaults:UPDATER_DEFAULTS]; + + // Save a copy of the default options, since iOS doesn't have + // [userDefaultsController initialValues]. + // + if (defaultOptions) + [defaultOptions release]; + defaultOptions = [[NSMutableDictionary dictionaryWithCapacity:20] + retain]; + for (NSString *key in defsdict) { + [defaultOptions setValue:[defsdict objectForKey:key] forKey:key]; + } +# ifndef USE_IPHONE userDefaultsController = [[NSUserDefaultsController alloc] initWithDefaults:userDefaults initialValues:defsdict]; + globalDefaultsController = + [[NSUserDefaultsController alloc] initWithDefaults:globalDefaults + initialValues:defsdict]; +# else // USE_IPHONE + userDefaultsController = userDefaults; + globalDefaultsController = userDefaults; +# endif // USE_IPHONE NSDictionary *optsdict = [NSMutableDictionary dictionaryWithCapacity:20]; @@ -100,15 +301,16 @@ while (*resource == '.' || *resource == '*') resource++; - NSString *nsresource = [NSString stringWithCString:resource - encoding:NSUTF8StringEncoding]; + NSString *nsresource = [self makeCKey:resource]; // make sure there's no resource mentioned in options and not defaults. if (![defsdict objectForKey:nsresource]) { - if (! (!strcmp(resource, "font") || // don't warn about these + if (! (!strcmp(resource, "font") || // don't warn about these + !strcmp(resource, "foreground") || !strcmp(resource, "textLiteral") || - !strcmp(resource, "textFile") || - !strcmp(resource, "textURL") || + !strcmp(resource, "textFile") || + !strcmp(resource, "textURL") || + !strcmp(resource, "textProgram") || !strcmp(resource, "imageDirectory"))) NSLog (@"warning: \"%s\" is in options but not defaults", resource); } @@ -117,11 +319,9 @@ opts++; } - // make sure there's no resource mentioned in defaults and not options. - NSEnumerator *enumerator = [defsdict keyEnumerator]; - NSString *key; - while ((key = [enumerator nextObject])) { #if 0 + // make sure there's no resource mentioned in defaults and not options. + for (NSString *key in defsdict) { if (! [optsdict objectForKey:key]) if (! ([key isEqualToString:@"foreground"] || // don't warn about these [key isEqualToString:@"background"] || @@ -142,8 +342,26 @@ [key isEqualToString:@"TVTint"] )) NSLog (@"warning: \"%@\" is in defaults but not options", key); + } #endif /* 0 */ + +#if 0 + // Dump the entire resource database. + NSLog(@"userDefaults:"); + NSDictionary *d = [userDefaults dictionaryRepresentation]; + for (NSObject *key in [[d allKeys] + sortedArrayUsingSelector:@selector(compare:)]) { + NSObject *val = [d objectForKey:key]; + NSLog (@"%@ = %@", key, val); + } + NSLog(@"globalDefaults:"); + d = [globalDefaults dictionaryRepresentation]; + for (NSObject *key in [[d allKeys] + sortedArrayUsingSelector:@selector(compare:)]) { + NSObject *val = [d objectForKey:key]; + NSLog (@"%@ = %@", key, val); } +#endif } @@ -153,24 +371,41 @@ return userDefaultsController; } +- (NSUserDefaultsController *) globalDefaultsController +{ + NSAssert(globalDefaultsController, @"globalDefaultsController uninitialized"); + return globalDefaultsController; +} + +- (NSDictionary *) defaultOptions +{ + NSAssert(defaultOptions, @"defaultOptions uninitialized"); + return defaultOptions; +} + - (NSObject *) getObjectResource: (const char *) name { - while (1) { - NSString *key = [NSString stringWithCString:name - encoding:NSUTF8StringEncoding]; - NSObject *obj = [userDefaults objectForKey:key]; - if (obj) - return obj; - - // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz". - // - const char *dot = strchr (name, '.'); - if (dot && dot[1]) - name = dot + 1; - else - return nil; + // First look in userDefaults, then in globalDefaults. + for (int globalp = 0; globalp <= 1; globalp++) { + const char *name2 = name; + while (1) { + NSString *key = [self makeCKey:name2]; + NSObject *obj = [(globalp ? globalDefaults : userDefaults) + objectForKey:key]; + if (obj) + return obj; + + // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz". + // + const char *dot = strchr (name2, '.'); + if (dot && dot[1]) + name2 = dot + 1; + else + break; + } } + return NULL; } @@ -188,26 +423,39 @@ !strcmp(name, "font") || !strcmp(name, "labelFont") || // grabclient.c !strcmp(name, "titleFont") || - !strcmp(name, "background") + !strcmp(name, "fpsFont") || // fps.c + !strcmp(name, "foreground") || // fps.c + !strcmp(name, "background") || + !strcmp(name, "textLiteral") )) NSLog(@"warning: no preference \"%s\" [string]", name); return NULL; } -#if 0 - if (! [o isKindOfClass:[NSString class]]) { - NSAssert2(0, @"%s = \"%@\" but should have been an NSString", name, o); - abort(); - } -#else if (! [o isKindOfClass:[NSString class]]) { NSLog(@"asked for %s as a string, but it is a %@", name, [o class]); o = [(NSNumber *) o stringValue]; } -#endif NSString *os = (NSString *) o; - const char *result = [os cStringUsingEncoding:NSUTF8StringEncoding]; - return strdup (result); + char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]); + + // Kludge: if the string is surrounded with single-quotes, remove them. + // This happens when the .xml file says things like arg="-foo 'bar baz'" + if (result[0] == '\'' && result[strlen(result)-1] == '\'') { + result[strlen(result)-1] = 0; + strcpy (result, result+1); + } + + // Kludge: assume that any string that begins with "~" and has a "/" + // anywhere in it should be expanded as if it is a pathname. + if (result[0] == '~' && strchr (result, '/')) { + os = [NSString stringWithCString:result encoding:NSUTF8StringEncoding]; + free (result); + result = strdup ([[os stringByExpandingTildeInPath] + cStringUsingEncoding:NSUTF8StringEncoding]); + } + + return result; } @@ -227,6 +475,7 @@ !strcmp(name, "mono") || !strcmp(name, "count") || !strcmp(name, "ncolors") || + !strcmp(name, "doFPS") || // fps.c !strcmp(name, "eraseSeconds") // erase.c )) NSLog(@"warning: no preference \"%s\" [float]", name); @@ -245,17 +494,40 @@ - (int) getIntegerResource: (const char *) name { - return (int) [self getFloatResource:name]; + // Sliders might store float values for integral resources; round them. + float v = [self getFloatResource:name]; + int i = (int) (v + (v < 0 ? -0.5 : 0.5)); // ignore sign or -1 rounds to 0 + // if (i != v) NSLog(@"%s: rounded %.3f to %d", name, v, i); + return i; } - (BOOL) getBooleanResource: (const char *) name { - int n = [self getIntegerResource:name]; - if (n == 0) return NO; - else if (n == 1) return YES; - else { - NSAssert2(0, @"%s = %d but should have been 0 or 1", name, n); + NSObject *o = [self getObjectResource:name]; + if (! o) { + return NO; + } else if ([o isKindOfClass:[NSNumber class]]) { + double n = [(NSNumber *) o doubleValue]; + if (n == 0) return NO; + else if (n == 1) return YES; + else goto FAIL; + } else if ([o isKindOfClass:[NSString class]]) { + NSString *s = [((NSString *) o) lowercaseString]; + if ([s isEqualToString:@"true"] || + [s isEqualToString:@"yes"] || + [s isEqualToString:@"1"]) + return YES; + else if ([s isEqualToString:@"false"] || + [s isEqualToString:@"no"] || + [s isEqualToString:@"0"] || + [s isEqualToString:@""]) + return NO; + else + goto FAIL; + } else { + FAIL: + NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o); abort(); } } @@ -268,7 +540,21 @@ self = [self init]; if (!self) return nil; +# ifndef USE_IPHONE userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name]; + globalDefaults = [[[GlobalDefaults alloc] initWithDomain:@UPDATER_DOMAIN] + retain]; +# else // USE_IPHONE + userDefaults = [NSUserDefaults standardUserDefaults]; + globalDefaults = userDefaults; +# endif // USE_IPHONE + + // Convert "org.jwz.xscreensaver.NAME" to just "NAME". + NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch]; + if (r.length) + name = [name substringFromIndex:r.location+1]; + name = [name stringByReplacingOccurrencesOfString:@" " withString:@""]; + saver_name = [name retain]; [self registerXrmKeys:opts defaults:defs]; return self; @@ -276,7 +562,9 @@ - (void) dealloc { + [saver_name release]; [userDefaultsController release]; + [globalDefaultsController release]; [super dealloc]; }