From http://www.jwz.org/xscreensaver/xscreensaver-5.35.tar.gz
[xscreensaver] / OSX / PrefsReader.m
1 /* xscreensaver, Copyright (c) 2006-2015 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)
101     [obj autorelease];
102   else if (defaults)
103     obj = [defaults objectForKey:key];
104   return obj;
105 }
106
107 - (void)setObject:(id)value forKey:(NSString *)key
108 {
109   if (value && defaults) {
110     // If the value is the default, then remove it instead.
111     NSObject *def = [defaults objectForKey:key];
112     if (def && [def isEqual:value])
113       value = NULL;
114   }
115   CFPreferencesSetAppValue ((CFStringRef) key,
116                             (CFPropertyListRef) value,
117                             (CFStringRef) domain);
118 }
119
120
121 - (BOOL)synchronize
122 {
123   return CFPreferencesAppSynchronize ((CFStringRef) domain);
124 }
125
126
127 // Make sure these all call our objectForKey.
128 // Might not be necessary, but safe.
129
130 - (NSString *)stringForKey:(NSString *)key
131 {
132   return [[self objectForKey:key] stringValue];
133 }
134
135 - (NSArray *)arrayForKey:(NSString *)key
136 {
137   return (NSArray *) [self objectForKey:key];
138 }
139
140 - (NSDictionary *)dictionaryForKey:(NSString *)key
141 {
142   return (NSDictionary *) [self objectForKey:key];
143 }
144
145 - (NSData *)dataForKey:(NSString *)key
146 {
147   return (NSData *) [self objectForKey:key];
148 }
149
150 - (NSArray *)stringArrayForKey:(NSString *)key
151 {
152   return (NSArray *) [self objectForKey:key];
153 }
154
155 - (NSInteger)integerForKey:(NSString *)key
156 {
157   return [[self objectForKey:key] integerValue];
158 }
159
160 - (float)floatForKey:(NSString *)key
161 {
162   return [[self objectForKey:key] floatValue];
163 }
164
165 - (double)doubleForKey:(NSString *)key
166 {
167   return [[self objectForKey:key] doubleValue];
168 }
169
170 - (BOOL)boolForKey:(NSString *)key
171 {
172   return [[self objectForKey:key] integerValue];
173 }
174
175 // Make sure these all call our setObject.
176 // Might not be necessary, but safe.
177
178 - (void)removeObjectForKey:(NSString *)key
179 {
180   [self setObject:NULL forKey:key];
181 }
182
183 - (void)setInteger:(NSInteger)value forKey:(NSString *)key
184 {
185   [self setObject:[NSNumber numberWithInteger:value] forKey:key];
186 }
187
188 - (void)setFloat:(float)value forKey:(NSString *)key
189 {
190   [self setObject:[NSNumber numberWithFloat:value] forKey:key];
191 }
192
193 - (void)setDouble:(double)value forKey:(NSString *)key
194 {
195   [self setObject:[NSNumber numberWithDouble:value] forKey:key];
196 }
197
198 - (void)setBool:(BOOL)value forKey:(NSString *)key
199 {
200   [self setObject:[NSNumber numberWithBool:value] forKey:key];
201 }
202 @end
203
204
205 #endif // !USE_IPHONE
206
207
208 @implementation PrefsReader
209
210 /* Normally we read resources by looking up "KEY" in the database
211    "org.jwz.xscreensaver.SAVERNAME".  But in the all-in-one iPhone
212    app, everything is stored in the database "org.jwz.xscreensaver"
213    instead, so transform keys to "SAVERNAME.KEY".
214
215    NOTE: This is duplicated in XScreenSaverConfigSheet.m, cause I suck.
216  */
217 - (NSString *) makeKey:(NSString *)key
218 {
219 # ifdef USE_IPHONE
220   NSString *prefix = [saver_name stringByAppendingString:@"."];
221   if (! [key hasPrefix:prefix])  // Don't double up!
222     key = [prefix stringByAppendingString:key];
223 # endif
224   return key;
225 }
226
227 - (NSString *) makeCKey:(const char *)key
228 {
229   return [self makeKey:[NSString stringWithCString:key
230                                  encoding:NSUTF8StringEncoding]];
231 }
232
233
234 /* Converts an array of "key:value" strings to an NSDictionary.
235  */
236 - (NSDictionary *) defaultsToDict: (const char * const *) defs
237 {
238   NSDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:20];
239   while (*defs) {
240     char *line = strdup (*defs);
241     char *key, *val;
242     key = line;
243     while (*key == '.' || *key == '*' || *key == ' ' || *key == '\t')
244       key++;
245     val = key;
246     while (*val && *val != ':')
247       val++;
248     if (*val != ':') abort();
249     *val++ = 0;
250     while (*val == ' ' || *val == '\t')
251       val++;
252
253     unsigned long L = strlen(val);
254     while (L > 0 && (val[L-1] == ' ' || val[L-1] == '\t'))
255       val[--L] = 0;
256
257     // When storing into preferences, look at the default string and
258     // decide whether it's a boolean, int, float, or string, and store
259     // an object of the appropriate type in the prefs.
260     //
261     NSString *nskey = [self makeCKey:key];
262     NSObject *nsval;
263     int dd;
264     double ff;
265     char cc;
266     if (!strcasecmp (val, "true") || !strcasecmp (val, "yes"))
267       nsval = [NSNumber numberWithBool:YES];
268     else if (!strcasecmp (val, "false") || !strcasecmp (val, "no"))
269       nsval = [NSNumber numberWithBool:NO];
270     else if (1 == sscanf (val, " %d %c", &dd, &cc))
271       nsval = [NSNumber numberWithInt:dd];
272     else if (1 == sscanf (val, " %lf %c", &ff, &cc))
273       nsval = [NSNumber numberWithDouble:ff];
274     else
275       nsval = [NSString stringWithCString:val encoding:NSUTF8StringEncoding];
276       
277 //    NSLog (@"default: \"%@\" = \"%@\" [%@]", nskey, nsval, [nsval class]);
278     [dict setValue:nsval forKey:nskey];
279     free (line);
280     defs++;
281   }
282   return dict;
283 }
284
285
286 /* Initialize the Cocoa preferences database:
287    - sets the default preferences values from the 'defaults' array;
288    - binds 'self' to each preference as an observer;
289    - ensures that nothing is mentioned in 'options' and not in 'defaults';
290    - ensures that nothing is mentioned in 'defaults' and not in 'options'.
291  */
292 - (void) registerXrmKeys: (const XrmOptionDescRec *) opts
293                 defaults: (const char * const *) defs
294 {
295   // Store the contents of 'defaults' into the real preferences database.
296   NSDictionary *defsdict = [self defaultsToDict:defs];
297   [userDefaults registerDefaults:defsdict];
298   [globalDefaults registerDefaults:UPDATER_DEFAULTS];
299
300   // Save a copy of the default options, since iOS doesn't have
301   // [userDefaultsController initialValues].
302   //
303   if (defaultOptions) 
304     [defaultOptions release];
305   defaultOptions = [[NSMutableDictionary dictionaryWithCapacity:20]
306                      retain];
307   for (NSString *key in defsdict) {
308     [defaultOptions setValue:[defsdict objectForKey:key] forKey:key];
309   }
310
311 # ifndef USE_IPHONE
312   userDefaultsController = 
313     [[NSUserDefaultsController alloc] initWithDefaults:userDefaults
314                                       initialValues:defsdict];
315   globalDefaultsController = 
316     [[NSUserDefaultsController alloc] initWithDefaults:globalDefaults
317                                       initialValues:UPDATER_DEFAULTS];
318 # else  // USE_IPHONE
319   userDefaultsController   = [userDefaults retain];
320   globalDefaultsController = [userDefaults retain];
321 # endif // USE_IPHONE
322
323   NSDictionary *optsdict = [NSMutableDictionary dictionaryWithCapacity:20];
324
325   while (opts[0].option) {
326     //const char *option   = opts->option;
327     const char *resource = opts->specifier;
328     
329     while (*resource == '.' || *resource == '*')
330       resource++;
331     NSString *nsresource = [self makeCKey:resource];
332     
333     // make sure there's no resource mentioned in options and not defaults.
334     if (![defsdict objectForKey:nsresource]) {
335       if (! (!strcmp(resource, "font")        ||    // don't warn about these
336              !strcmp(resource, "foreground")  ||
337              !strcmp(resource, "textLiteral") ||
338              !strcmp(resource, "textFile")    ||
339              !strcmp(resource, "textURL")     ||
340              !strcmp(resource, "textProgram") ||
341              !strcmp(resource, "imageDirectory")))
342         NSLog (@"warning: \"%s\" is in options but not defaults", resource);
343     }
344     [optsdict setValue:nsresource forKey:nsresource];
345     
346     opts++;
347   }
348
349 #if 0
350   // make sure there's no resource mentioned in defaults and not options.
351   for (NSString *key in defsdict) {
352     if (! [optsdict objectForKey:key])
353       if (! ([key isEqualToString:@"foreground"] || // don't warn about these
354              [key isEqualToString:@"background"] ||
355              [key isEqualToString:@"Background"] ||
356              [key isEqualToString:@"geometry"] ||
357              [key isEqualToString:@"font"] ||
358              [key isEqualToString:@"dontClearRoot"] ||
359
360              // fps.c settings
361              [key isEqualToString:@"fpsSolid"] ||
362              [key isEqualToString:@"fpsTop"] ||
363              [key isEqualToString:@"titleFont"] ||
364
365              // analogtv.c settings
366              [key isEqualToString:@"TVBrightness"] ||
367              [key isEqualToString:@"TVColor"] ||
368              [key isEqualToString:@"TVContrast"] ||
369              [key isEqualToString:@"TVTint"]
370              ))
371       NSLog (@"warning: \"%@\" is in defaults but not options", key);
372   }
373 #endif /* 0 */
374
375 #if 0
376   // Dump the entire resource database.
377   NSLog(@"userDefaults:");
378   NSDictionary *d = [userDefaults dictionaryRepresentation];
379   for (NSObject *key in [[d allKeys]
380                           sortedArrayUsingSelector:@selector(compare:)]) {
381     NSObject *val = [d objectForKey:key];
382     NSLog (@"%@ = %@", key, val);
383   }
384   NSLog(@"globalDefaults:");
385   d = [globalDefaults dictionaryRepresentation];
386   for (NSObject *key in [[d allKeys]
387                           sortedArrayUsingSelector:@selector(compare:)]) {
388     NSObject *val = [d objectForKey:key];
389     NSLog (@"%@ = %@", key, val);
390   }
391 #endif
392
393 }
394
395 - (NSUserDefaultsController *) userDefaultsController
396 {
397   NSAssert(userDefaultsController, @"userDefaultsController uninitialized");
398   return userDefaultsController;
399 }
400
401 - (NSUserDefaultsController *) globalDefaultsController
402 {
403   NSAssert(globalDefaultsController, @"globalDefaultsController uninitialized");
404   return globalDefaultsController;
405 }
406
407 - (NSDictionary *) defaultOptions
408 {
409   NSAssert(defaultOptions, @"defaultOptions uninitialized");
410   return defaultOptions;
411 }
412
413
414 - (NSObject *) getObjectResource: (const char *) name
415 {
416   // Only look in globalDefaults for updater preferences.
417
418   static NSDictionary *updaterDefaults;
419   if (!updaterDefaults) {
420     updaterDefaults = UPDATER_DEFAULTS;
421     [updaterDefaults retain];
422   }
423   
424   NSUserDefaults *defaults =
425     [updaterDefaults objectForKey:[NSString stringWithUTF8String:name]] ?
426     globalDefaults :
427     userDefaults;
428   
429   const char *name2 = name;
430   while (1) {
431     NSString *key = [self makeCKey:name2];
432     NSObject *obj = [defaults objectForKey:key];
433     if (obj)
434       return obj;
435
436     // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz".
437     //
438     const char *dot = strchr (name2, '.');
439     if (dot && dot[1])
440       name2 = dot + 1;
441     else
442       break;
443   }
444   return NULL;
445 }
446
447
448 - (char *) getStringResource: (const char *) name
449 {
450   NSObject *o = [self getObjectResource:name];
451   //NSLog(@"%s = %@",name,o);
452   if (o == nil) {
453     if (! (!strcmp(name, "eraseMode") || // erase.c
454            // xlockmore.c reads all of these whether used or not...
455            !strcmp(name, "right3d") ||
456            !strcmp(name, "left3d") ||
457            !strcmp(name, "both3d") ||
458            !strcmp(name, "none3d") ||
459            !strcmp(name, "font") ||
460            !strcmp(name, "labelFont") ||  // grabclient.c
461            !strcmp(name, "titleFont") ||
462            !strcmp(name, "fpsFont") ||    // fps.c
463            !strcmp(name, "foreground") || // fps.c
464            !strcmp(name, "background") ||
465            !strcmp(name, "textLiteral")
466            ))
467       NSLog(@"warning: no preference \"%s\" [string]", name);
468     return NULL;
469   }
470   if (! [o isKindOfClass:[NSString class]]) {
471     NSLog(@"asked for %s as a string, but it is a %@", name, [o class]);
472     o = [(NSNumber *) o stringValue];
473   }
474
475   NSString *os = (NSString *) o;
476   char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]);
477
478   // Kludge: if the string is surrounded with single-quotes, remove them.
479   // This happens when the .xml file says things like arg="-foo 'bar baz'"
480   if (result[0] == '\'' && result[strlen(result)-1] == '\'') {
481     result[strlen(result)-1] = 0;
482     strcpy (result, result+1);
483   }
484
485   // Kludge: assume that any string that begins with "~" and has a "/"
486   // anywhere in it should be expanded as if it is a pathname.
487   if (result[0] == '~' && strchr (result, '/')) {
488     os = [NSString stringWithCString:result encoding:NSUTF8StringEncoding];
489     free (result);
490     result = strdup ([[os stringByExpandingTildeInPath]
491                        cStringUsingEncoding:NSUTF8StringEncoding]);
492   }
493
494   return result;
495 }
496
497
498 - (double) getFloatResource: (const char *) name
499 {
500   NSObject *o = [self getObjectResource:name];
501   if (o == nil) {
502     // xlockmore.c reads all of these whether used or not...
503     if (! (!strcmp(name, "cycles") ||
504            !strcmp(name, "size") ||
505            !strcmp(name, "use3d") ||
506            !strcmp(name, "delta3d") ||
507            !strcmp(name, "wireframe") ||
508            !strcmp(name, "showFPS") ||
509            !strcmp(name, "fpsSolid") ||
510            !strcmp(name, "fpsTop") ||
511            !strcmp(name, "mono") ||
512            !strcmp(name, "count") ||
513            !strcmp(name, "ncolors") ||
514            !strcmp(name, "doFPS") ||      // fps.c
515            !strcmp(name, "eraseSeconds")  // erase.c
516            ))
517       NSLog(@"warning: no preference \"%s\" [float]", name);
518     return 0.0;
519   }
520   if ([o isKindOfClass:[NSString class]]) {
521     return [(NSString *) o doubleValue];
522   } else if ([o isKindOfClass:[NSNumber class]]) {
523     return [(NSNumber *) o doubleValue];
524   } else {
525     NSAssert2(0, @"%s = \"%@\" but should have been an NSNumber", name, o);
526     abort();
527   }
528 }
529
530
531 - (int) getIntegerResource: (const char *) name
532 {
533   // Sliders might store float values for integral resources; round them.
534   float v = [self getFloatResource:name];
535   int i = (int) (v + (v < 0 ? -0.5 : 0.5)); // ignore sign or -1 rounds to 0
536   // if (i != v) NSLog(@"%s: rounded %.3f to %d", name, v, i);
537   return i;
538 }
539
540
541 - (BOOL) getBooleanResource: (const char *) name
542 {
543   NSObject *o = [self getObjectResource:name];
544   if (! o) {
545     return NO;
546   } else if ([o isKindOfClass:[NSNumber class]]) {
547     double n = [(NSNumber *) o doubleValue];
548     if (n == 0) return NO;
549     else if (n == 1) return YES;
550     else goto FAIL;
551   } else if ([o isKindOfClass:[NSString class]]) {
552     NSString *s = [((NSString *) o) lowercaseString];
553     if ([s isEqualToString:@"true"] ||
554         [s isEqualToString:@"yes"] ||
555         [s isEqualToString:@"1"])
556       return YES;
557     else if ([s isEqualToString:@"false"] ||
558              [s isEqualToString:@"no"] ||
559              [s isEqualToString:@"0"] ||
560              [s isEqualToString:@""])
561       return NO;
562     else
563       goto FAIL;
564   } else {
565   FAIL:
566     NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o);
567     abort();
568   }
569 }
570
571
572 - (id) initWithName: (NSString *) name
573             xrmKeys: (const XrmOptionDescRec *) opts
574            defaults: (const char * const *) defs
575 {
576   self = [self init];
577   if (!self) return nil;
578
579 # ifndef USE_IPHONE
580   userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name];
581   globalDefaults = [[GlobalDefaults alloc] initWithDomain:@UPDATER_DOMAIN
582                                                    module:name];
583 # else  // USE_IPHONE
584   userDefaults = [NSUserDefaults standardUserDefaults];
585   globalDefaults = [userDefaults retain];
586 # endif // USE_IPHONE
587
588   // Convert "org.jwz.xscreensaver.NAME" to just "NAME".
589   NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch];
590   if (r.length)
591     name = [name substringFromIndex:r.location+1];
592   name = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
593   saver_name = [name retain];
594
595   [self registerXrmKeys:opts defaults:defs];
596   return self;
597 }
598
599 - (void) dealloc
600 {
601   [saver_name release];
602   [userDefaultsController release];
603   [globalDefaultsController release];
604   [globalDefaults release];
605   [super dealloc];
606 }
607
608 @end