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