From http://www.jwz.org/xscreensaver/xscreensaver-5.30.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:UPDATER_DEFAULTS];
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   // Only look in globalDefaults for updater preferences.
415
416   static NSDictionary *updaterDefaults;
417   if (!updaterDefaults) {
418     updaterDefaults = UPDATER_DEFAULTS;
419     [updaterDefaults retain];
420   }
421   
422   NSUserDefaults *defaults =
423     [updaterDefaults objectForKey:[NSString stringWithUTF8String:name]] ?
424     globalDefaults :
425     userDefaults;
426   
427   const char *name2 = name;
428   while (1) {
429     NSString *key = [self makeCKey:name2];
430     NSObject *obj = [defaults objectForKey:key];
431     if (obj)
432       return obj;
433
434     // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz".
435     //
436     const char *dot = strchr (name2, '.');
437     if (dot && dot[1])
438       name2 = dot + 1;
439     else
440       break;
441   }
442   return NULL;
443 }
444
445
446 - (char *) getStringResource: (const char *) name
447 {
448   NSObject *o = [self getObjectResource:name];
449   //NSLog(@"%s = %@",name,o);
450   if (o == nil) {
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")
464            ))
465       NSLog(@"warning: no preference \"%s\" [string]", name);
466     return NULL;
467   }
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];
471   }
472
473   NSString *os = (NSString *) o;
474   char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]);
475
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);
481   }
482
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];
487     free (result);
488     result = strdup ([[os stringByExpandingTildeInPath]
489                        cStringUsingEncoding:NSUTF8StringEncoding]);
490   }
491
492   return result;
493 }
494
495
496 - (double) getFloatResource: (const char *) name
497 {
498   NSObject *o = [self getObjectResource:name];
499   if (o == nil) {
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
514            ))
515       NSLog(@"warning: no preference \"%s\" [float]", name);
516     return 0.0;
517   }
518   if ([o isKindOfClass:[NSString class]]) {
519     return [(NSString *) o doubleValue];
520   } else if ([o isKindOfClass:[NSNumber class]]) {
521     return [(NSNumber *) o doubleValue];
522   } else {
523     NSAssert2(0, @"%s = \"%@\" but should have been an NSNumber", name, o);
524     abort();
525   }
526 }
527
528
529 - (int) getIntegerResource: (const char *) name
530 {
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);
535   return i;
536 }
537
538
539 - (BOOL) getBooleanResource: (const char *) name
540 {
541   NSObject *o = [self getObjectResource:name];
542   if (! o) {
543     return NO;
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;
548     else goto FAIL;
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"])
554       return YES;
555     else if ([s isEqualToString:@"false"] ||
556              [s isEqualToString:@"no"] ||
557              [s isEqualToString:@"0"] ||
558              [s isEqualToString:@""])
559       return NO;
560     else
561       goto FAIL;
562   } else {
563   FAIL:
564     NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o);
565     abort();
566   }
567 }
568
569
570 - (id) initWithName: (NSString *) name
571             xrmKeys: (const XrmOptionDescRec *) opts
572            defaults: (const char * const *) defs
573 {
574   self = [self init];
575   if (!self) return nil;
576
577 # ifndef USE_IPHONE
578   userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name];
579   globalDefaults = [[GlobalDefaults alloc] initWithDomain:@UPDATER_DOMAIN
580                                                    module:name];
581 # else  // USE_IPHONE
582   userDefaults = [NSUserDefaults standardUserDefaults];
583   globalDefaults = [userDefaults retain];
584 # endif // USE_IPHONE
585
586   // Convert "org.jwz.xscreensaver.NAME" to just "NAME".
587   NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch];
588   if (r.length)
589     name = [name substringFromIndex:r.location+1];
590   name = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
591   saver_name = [name retain];
592
593   [self registerXrmKeys:opts defaults:defs];
594   return self;
595 }
596
597 - (void) dealloc
598 {
599   [saver_name release];
600   [userDefaultsController release];
601   [globalDefaultsController release];
602   [globalDefaults release];
603   [super dealloc];
604 }
605
606 @end