From http://www.jwz.org/xscreensaver/xscreensaver-5.27.tar.gz
[xscreensaver] / OSX / PrefsReader.m
1 /* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski <jwz@jwz.org>
2  *
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 
9  * implied warranty.
10  */
11
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).
17  */
18
19 #ifndef USE_IPHONE
20 # import <ScreenSaver/ScreenSaverDefaults.h>
21 #endif
22
23 #import "PrefsReader.h"
24 #import "Updater.h"
25 #import "screenhackI.h"
26
27 #ifndef USE_IPHONE
28
29 #include <objc/runtime.h>
30
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.
35
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.
40
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.
44  */
45 @interface GlobalDefaults : NSUserDefaults
46 {
47   NSString *domain;
48   NSDictionary *defaults;
49 }
50 @end
51
52 @implementation GlobalDefaults
53 - (id) initWithDomain:(NSString *)_domain module:(NSString *)_module
54 {
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.
60
61   // This gives the class a unique name, sidestepping the above issue.
62
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.
66   char class_name[128];
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);
70   if (!c)
71     return nil;
72   objc_registerClassPair(c);
73
74   self = [super init];
75   object_setClass(self, c);
76   domain = [_domain retain];
77   return self;
78 }
79
80 - (void) dealloc
81 {
82   Class c = object_getClass(self);
83
84   [domain release];
85   [defaults release];
86   [super dealloc];
87
88   objc_disposeClassPair(c);
89 }
90
91 - (void)registerDefaults:(NSDictionary *)dict
92 {
93   defaults = [dict retain];
94 }
95
96 - (id)objectForKey:(NSString *)key
97 {
98   NSObject *obj = (NSObject *)
99     CFPreferencesCopyAppValue ((CFStringRef) key, (CFStringRef) domain);
100   if (!obj && defaults)
101     obj = [defaults objectForKey:key];
102   return obj;
103 }
104
105 - (void)setObject:(id)value forKey:(NSString *)key
106 {
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])
111       value = NULL;
112   }
113   CFPreferencesSetAppValue ((CFStringRef) key,
114                             (CFPropertyListRef) value,
115                             (CFStringRef) domain);
116 }
117
118
119 - (BOOL)synchronize
120 {
121   return CFPreferencesAppSynchronize ((CFStringRef) domain);
122 }
123
124
125 // Make sure these all call our objectForKey.
126 // Might not be necessary, but safe.
127
128 - (NSString *)stringForKey:(NSString *)key
129 {
130   return [[self objectForKey:key] stringValue];
131 }
132
133 - (NSArray *)arrayForKey:(NSString *)key
134 {
135   return (NSArray *) [self objectForKey:key];
136 }
137
138 - (NSDictionary *)dictionaryForKey:(NSString *)key
139 {
140   return (NSDictionary *) [self objectForKey:key];
141 }
142
143 - (NSData *)dataForKey:(NSString *)key
144 {
145   return (NSData *) [self objectForKey:key];
146 }
147
148 - (NSArray *)stringArrayForKey:(NSString *)key
149 {
150   return (NSArray *) [self objectForKey:key];
151 }
152
153 - (NSInteger)integerForKey:(NSString *)key
154 {
155   return [[self objectForKey:key] integerValue];
156 }
157
158 - (float)floatForKey:(NSString *)key
159 {
160   return [[self objectForKey:key] floatValue];
161 }
162
163 - (double)doubleForKey:(NSString *)key
164 {
165   return [[self objectForKey:key] doubleValue];
166 }
167
168 - (BOOL)boolForKey:(NSString *)key
169 {
170   return [[self objectForKey:key] integerValue];
171 }
172
173 // Make sure these all call our setObject.
174 // Might not be necessary, but safe.
175
176 - (void)removeObjectForKey:(NSString *)key
177 {
178   [self setObject:NULL forKey:key];
179 }
180
181 - (void)setInteger:(NSInteger)value forKey:(NSString *)key
182 {
183   [self setObject:[NSNumber numberWithInteger:value] forKey:key];
184 }
185
186 - (void)setFloat:(float)value forKey:(NSString *)key
187 {
188   [self setObject:[NSNumber numberWithFloat:value] forKey:key];
189 }
190
191 - (void)setDouble:(double)value forKey:(NSString *)key
192 {
193   [self setObject:[NSNumber numberWithDouble:value] forKey:key];
194 }
195
196 - (void)setBool:(BOOL)value forKey:(NSString *)key
197 {
198   [self setObject:[NSNumber numberWithBool:value] forKey:key];
199 }
200 @end
201
202
203 #endif // !USE_IPHONE
204
205
206 @implementation PrefsReader
207
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".
212
213    NOTE: This is duplicated in XScreenSaverConfigSheet.m, cause I suck.
214  */
215 - (NSString *) makeKey:(NSString *)key
216 {
217 # ifdef USE_IPHONE
218   NSString *prefix = [saver_name stringByAppendingString:@"."];
219   if (! [key hasPrefix:prefix])  // Don't double up!
220     key = [prefix stringByAppendingString:key];
221 # endif
222   return key;
223 }
224
225 - (NSString *) makeCKey:(const char *)key
226 {
227   return [self makeKey:[NSString stringWithCString:key
228                                  encoding:NSUTF8StringEncoding]];
229 }
230
231
232 /* Converts an array of "key:value" strings to an NSDictionary.
233  */
234 - (NSDictionary *) defaultsToDict: (const char * const *) defs
235 {
236   NSDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:20];
237   while (*defs) {
238     char *line = strdup (*defs);
239     char *key, *val;
240     key = line;
241     while (*key == '.' || *key == '*' || *key == ' ' || *key == '\t')
242       key++;
243     val = key;
244     while (*val && *val != ':')
245       val++;
246     if (*val != ':') abort();
247     *val++ = 0;
248     while (*val == ' ' || *val == '\t')
249       val++;
250
251     unsigned long L = strlen(val);
252     while (L > 0 && (val[L-1] == ' ' || val[L-1] == '\t'))
253       val[--L] = 0;
254
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.
258     //
259     NSString *nskey = [self makeCKey:key];
260     NSObject *nsval;
261     int dd;
262     double ff;
263     char cc;
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];
272     else
273       nsval = [NSString stringWithCString:val encoding:NSUTF8StringEncoding];
274       
275 //    NSLog (@"default: \"%@\" = \"%@\" [%@]", nskey, nsval, [nsval class]);
276     [dict setValue:nsval forKey:nskey];
277     free (line);
278     defs++;
279   }
280   return dict;
281 }
282
283
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'.
289  */
290 - (void) registerXrmKeys: (const XrmOptionDescRec *) opts
291                 defaults: (const char * const *) defs
292 {
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];
297
298   // Save a copy of the default options, since iOS doesn't have
299   // [userDefaultsController initialValues].
300   //
301   if (defaultOptions) 
302     [defaultOptions release];
303   defaultOptions = [[NSMutableDictionary dictionaryWithCapacity:20]
304                      retain];
305   for (NSString *key in defsdict) {
306     [defaultOptions setValue:[defsdict objectForKey:key] forKey:key];
307   }
308
309 # ifndef USE_IPHONE
310   userDefaultsController = 
311     [[NSUserDefaultsController alloc] initWithDefaults:userDefaults
312                                       initialValues:defsdict];
313   globalDefaultsController = 
314     [[NSUserDefaultsController alloc] initWithDefaults:globalDefaults
315                                       initialValues:defsdict];
316 # else  // USE_IPHONE
317   userDefaultsController   = [userDefaults retain];
318   globalDefaultsController = [userDefaults retain];
319 # endif // USE_IPHONE
320
321   NSDictionary *optsdict = [NSMutableDictionary dictionaryWithCapacity:20];
322
323   while (opts[0].option) {
324     //const char *option   = opts->option;
325     const char *resource = opts->specifier;
326     
327     while (*resource == '.' || *resource == '*')
328       resource++;
329     NSString *nsresource = [self makeCKey:resource];
330     
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);
341     }
342     [optsdict setValue:nsresource forKey:nsresource];
343     
344     opts++;
345   }
346
347 #if 0
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"] ||
357
358              // fps.c settings
359              [key isEqualToString:@"fpsSolid"] ||
360              [key isEqualToString:@"fpsTop"] ||
361              [key isEqualToString:@"titleFont"] ||
362
363              // analogtv.c settings
364              [key isEqualToString:@"TVBrightness"] ||
365              [key isEqualToString:@"TVColor"] ||
366              [key isEqualToString:@"TVContrast"] ||
367              [key isEqualToString:@"TVTint"]
368              ))
369       NSLog (@"warning: \"%@\" is in defaults but not options", key);
370   }
371 #endif /* 0 */
372
373 #if 0
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);
381   }
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);
388   }
389 #endif
390
391 }
392
393 - (NSUserDefaultsController *) userDefaultsController
394 {
395   NSAssert(userDefaultsController, @"userDefaultsController uninitialized");
396   return userDefaultsController;
397 }
398
399 - (NSUserDefaultsController *) globalDefaultsController
400 {
401   NSAssert(globalDefaultsController, @"globalDefaultsController uninitialized");
402   return globalDefaultsController;
403 }
404
405 - (NSDictionary *) defaultOptions
406 {
407   NSAssert(defaultOptions, @"defaultOptions uninitialized");
408   return defaultOptions;
409 }
410
411
412 - (NSObject *) getObjectResource: (const char *) name
413 {
414   // First look in userDefaults, then in globalDefaults.
415   for (int globalp = 0; globalp <= 1; globalp++) {
416     const char *name2 = name;
417     while (1) {
418       NSString *key = [self makeCKey:name2];
419       NSObject *obj = [(globalp ? globalDefaults : userDefaults)
420                         objectForKey:key];
421       if (obj)
422         return obj;
423
424       // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz".
425       //
426       const char *dot = strchr (name2, '.');
427       if (dot && dot[1])
428         name2 = dot + 1;
429       else
430         break;
431     }
432   }
433   return NULL;
434 }
435
436
437 - (char *) getStringResource: (const char *) name
438 {
439   NSObject *o = [self getObjectResource:name];
440   //NSLog(@"%s = %@",name,o);
441   if (o == nil) {
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")
455            ))
456       NSLog(@"warning: no preference \"%s\" [string]", name);
457     return NULL;
458   }
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];
462   }
463
464   NSString *os = (NSString *) o;
465   char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]);
466
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);
472   }
473
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];
478     free (result);
479     result = strdup ([[os stringByExpandingTildeInPath]
480                        cStringUsingEncoding:NSUTF8StringEncoding]);
481   }
482
483   return result;
484 }
485
486
487 - (double) getFloatResource: (const char *) name
488 {
489   NSObject *o = [self getObjectResource:name];
490   if (o == nil) {
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
505            ))
506       NSLog(@"warning: no preference \"%s\" [float]", name);
507     return 0.0;
508   }
509   if ([o isKindOfClass:[NSString class]]) {
510     return [(NSString *) o doubleValue];
511   } else if ([o isKindOfClass:[NSNumber class]]) {
512     return [(NSNumber *) o doubleValue];
513   } else {
514     NSAssert2(0, @"%s = \"%@\" but should have been an NSNumber", name, o);
515     abort();
516   }
517 }
518
519
520 - (int) getIntegerResource: (const char *) name
521 {
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);
526   return i;
527 }
528
529
530 - (BOOL) getBooleanResource: (const char *) name
531 {
532   NSObject *o = [self getObjectResource:name];
533   if (! o) {
534     return NO;
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;
539     else goto FAIL;
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"])
545       return YES;
546     else if ([s isEqualToString:@"false"] ||
547              [s isEqualToString:@"no"] ||
548              [s isEqualToString:@"0"] ||
549              [s isEqualToString:@""])
550       return NO;
551     else
552       goto FAIL;
553   } else {
554   FAIL:
555     NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o);
556     abort();
557   }
558 }
559
560
561 - (id) initWithName: (NSString *) name
562             xrmKeys: (const XrmOptionDescRec *) opts
563            defaults: (const char * const *) defs
564 {
565   self = [self init];
566   if (!self) return nil;
567
568 # ifndef USE_IPHONE
569   userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name];
570   globalDefaults = [[GlobalDefaults alloc] initWithDomain:@UPDATER_DOMAIN
571                                                    module:name];
572 # else  // USE_IPHONE
573   userDefaults = [NSUserDefaults standardUserDefaults];
574   globalDefaults = [userDefaults retain];
575 # endif // USE_IPHONE
576
577   // Convert "org.jwz.xscreensaver.NAME" to just "NAME".
578   NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch];
579   if (r.length)
580     name = [name substringFromIndex:r.location+1];
581   name = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
582   saver_name = [name retain];
583
584   [self registerXrmKeys:opts defaults:defs];
585   return self;
586 }
587
588 - (void) dealloc
589 {
590   [saver_name release];
591   [userDefaultsController release];
592   [globalDefaultsController release];
593   [globalDefaults release];
594   [super dealloc];
595 }
596
597 @end