From http://www.jwz.org/xscreensaver/xscreensaver-5.24.tar.gz
[xscreensaver] / OSX / PrefsReader.m
index 3c20f3dcf328db28ecbd24bba93b6a83b39a334a..cf1698ece73a964928f56bc7510617fa2f89bf6b 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
    the UI (XScreenSaverConfigSheet).
  */
 
-#import <ScreenSaver/ScreenSaverDefaults.h>
+#ifndef USE_IPHONE
+# import <ScreenSaver/ScreenSaverDefaults.h>
+#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
     // 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;
     else
       nsval = [NSString stringWithCString:val encoding:NSUTF8StringEncoding];
       
-//    NSLog (@"default: \"%@\" = \"%@\" [%@]\n", nskey, nsval, [nsval class]);
+//    NSLog (@"default: \"%@\" = \"%@\" [%@]", nskey, nsval, [nsval class]);
     [dict setValue:nsval forKey:nskey];
     free (line);
     defs++;
   // 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];
 
     
     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);
     }
     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"] ||
              [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
 
 }
 
   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;
 }
 
 
 - (char *) getStringResource: (const char *) name
 {
   NSObject *o = [self getObjectResource:name];
-  //NSLog(@"%s = %@\n",name,o);
+  //NSLog(@"%s = %@",name,o);
   if (o == nil) {
     if (! (!strcmp(name, "eraseMode") || // erase.c
            // xlockmore.c reads all of these whether used or not...
            !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]\n", name);
+      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;
 }
 
 
            !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]\n", name);
+      NSLog(@"warning: no preference \"%s\" [float]", name);
     return 0.0;
   }
   if ([o isKindOfClass:[NSString class]]) {
 
 - (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\n", 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();
   }
 }
   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;
 
 - (void) dealloc
 {
+  [saver_name release];
   [userDefaultsController release];
+  [globalDefaultsController release];
   [super dealloc];
 }