4f42fa90e9f3b8d289d73aec242d692597d61c40
[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 "screenhackI.h"
25
26 @implementation PrefsReader
27
28 /* Normally we read resources by looking up "KEY" in the database
29    "org.jwz.xscreensaver.SAVERNAME".  But in the all-in-one iPhone
30    app, everything is stored in the database "org.jwz.xscreensaver"
31    instead, so transform keys to "SAVERNAME.KEY".
32
33    NOTE: This is duplicated in XScreenSaverConfigSheet.m, cause I suck.
34 */
35 - (NSString *) makeKey:(NSString *)key
36 {
37 # ifdef USE_IPHONE
38   NSString *prefix = [saver_name stringByAppendingString:@"."];
39   if (! [key hasPrefix:prefix])  // Don't double up!
40     key = [prefix stringByAppendingString:key];
41 # endif
42   return key;
43 }
44
45 - (NSString *) makeCKey:(const char *)key
46 {
47   return [self makeKey:[NSString stringWithCString:key
48                                  encoding:NSUTF8StringEncoding]];
49 }
50
51
52 /* Converts an array of "key:value" strings to an NSDictionary.
53  */
54 - (NSDictionary *) defaultsToDict: (const char * const *) defs
55 {
56   NSDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:20];
57   while (*defs) {
58     char *line = strdup (*defs);
59     char *key, *val;
60     key = line;
61     while (*key == '.' || *key == '*' || *key == ' ' || *key == '\t')
62       key++;
63     val = key;
64     while (*val && *val != ':')
65       val++;
66     if (*val != ':') abort();
67     *val++ = 0;
68     while (*val == ' ' || *val == '\t')
69       val++;
70
71     int L = strlen(val);
72     while (L > 0 && (val[L-1] == ' ' || val[L-1] == '\t'))
73       val[--L] = 0;
74
75     // When storing into preferences, look at the default string and
76     // decide whether it's a boolean, int, float, or string, and store
77     // an object of the appropriate type in the prefs.
78     //
79     NSString *nskey = [self makeCKey:key];
80     NSObject *nsval;
81     int dd;
82     double ff;
83     char cc;
84     if (!strcasecmp (val, "true") || !strcasecmp (val, "yes"))
85       nsval = [NSNumber numberWithBool:YES];
86     else if (!strcasecmp (val, "false") || !strcasecmp (val, "no"))
87       nsval = [NSNumber numberWithBool:NO];
88     else if (1 == sscanf (val, " %d %c", &dd, &cc))
89       nsval = [NSNumber numberWithInt:dd];
90     else if (1 == sscanf (val, " %lf %c", &ff, &cc))
91       nsval = [NSNumber numberWithDouble:ff];
92     else
93       nsval = [NSString stringWithCString:val encoding:NSUTF8StringEncoding];
94       
95 //    NSLog (@"default: \"%@\" = \"%@\" [%@]", nskey, nsval, [nsval class]);
96     [dict setValue:nsval forKey:nskey];
97     free (line);
98     defs++;
99   }
100   return dict;
101 }
102
103
104 /* Initialize the Cocoa preferences database:
105    - sets the default preferences values from the 'defaults' array;
106    - binds 'self' to each preference as an observer;
107    - ensures that nothing is mentioned in 'options' and not in 'defaults';
108    - ensures that nothing is mentioned in 'defaults' and not in 'options'.
109  */
110 - (void) registerXrmKeys: (const XrmOptionDescRec *) opts
111                 defaults: (const char * const *) defs
112 {
113   // Store the contents of 'defaults' into the real preferences database.
114   NSDictionary *defsdict = [self defaultsToDict:defs];
115   [userDefaults registerDefaults:defsdict];
116
117   // Save a copy of the default options, since iOS doesn't have
118   // [userDefaultsController initialValues].
119   //
120   if (defaultOptions) 
121     [defaultOptions release];
122   defaultOptions = [[NSMutableDictionary dictionaryWithCapacity:20]
123                      retain];
124   for (NSString *key in defsdict) {
125     [defaultOptions setValue:[defsdict objectForKey:key] forKey:key];
126   }
127
128 # ifndef USE_IPHONE
129   userDefaultsController = 
130     [[NSUserDefaultsController alloc] initWithDefaults:userDefaults
131                                       initialValues:defsdict];
132 # else  // USE_IPHONE
133   userDefaultsController = userDefaults;
134 # endif // USE_IPHONE
135
136   NSDictionary *optsdict = [NSMutableDictionary dictionaryWithCapacity:20];
137
138   while (opts[0].option) {
139     //const char *option   = opts->option;
140     const char *resource = opts->specifier;
141     
142     while (*resource == '.' || *resource == '*')
143       resource++;
144     NSString *nsresource = [self makeCKey:resource];
145     
146     // make sure there's no resource mentioned in options and not defaults.
147     if (![defsdict objectForKey:nsresource]) {
148       if (! (!strcmp(resource, "font")        ||    // don't warn about these
149              !strcmp(resource, "foreground")  ||
150              !strcmp(resource, "textLiteral") ||
151              !strcmp(resource, "textFile")    ||
152              !strcmp(resource, "textURL")     ||
153              !strcmp(resource, "textProgram") ||
154              !strcmp(resource, "imageDirectory")))
155         NSLog (@"warning: \"%s\" is in options but not defaults", resource);
156     }
157     [optsdict setValue:nsresource forKey:nsresource];
158     
159     opts++;
160   }
161
162 #if 0
163   // make sure there's no resource mentioned in defaults and not options.
164   for (NSString *key in defsdict) {
165     if (! [optsdict objectForKey:key])
166       if (! ([key isEqualToString:@"foreground"] || // don't warn about these
167              [key isEqualToString:@"background"] ||
168              [key isEqualToString:@"Background"] ||
169              [key isEqualToString:@"geometry"] ||
170              [key isEqualToString:@"font"] ||
171              [key isEqualToString:@"dontClearRoot"] ||
172
173              // fps.c settings
174              [key isEqualToString:@"fpsSolid"] ||
175              [key isEqualToString:@"fpsTop"] ||
176              [key isEqualToString:@"titleFont"] ||
177
178              // analogtv.c settings
179              [key isEqualToString:@"TVBrightness"] ||
180              [key isEqualToString:@"TVColor"] ||
181              [key isEqualToString:@"TVContrast"] ||
182              [key isEqualToString:@"TVTint"]
183              ))
184       NSLog (@"warning: \"%@\" is in defaults but not options", key);
185   }
186 #endif /* 0 */
187
188 #if 0
189   // Dump the entire resource database.
190   NSDictionary *d = [userDefaults dictionaryRepresentation];
191   for (NSObject *key in [[d allKeys]
192                           sortedArrayUsingSelector:@selector(compare:)]) {
193     NSObject *val = [d objectForKey:key];
194     NSLog (@"%@ = %@", key, val);
195   }
196 #endif
197
198 }
199
200 - (NSUserDefaultsController *) userDefaultsController
201 {
202   NSAssert(userDefaultsController, @"userDefaultsController uninitialized");
203   return userDefaultsController;
204 }
205
206 - (NSDictionary *) defaultOptions
207 {
208   NSAssert(defaultOptions, @"userDefaultsController uninitialized");
209   return defaultOptions;
210 }
211
212
213 - (NSObject *) getObjectResource: (const char *) name
214 {
215   while (1) {
216     NSString *key = [self makeCKey:name];
217     NSObject *obj = [userDefaults objectForKey:key];
218     if (obj)
219       return obj;
220
221     // If key is "foo.bar.baz", check "foo.bar.baz", "bar.baz", and "baz".
222     //
223     const char *dot = strchr (name, '.');
224     if (dot && dot[1])
225       name = dot + 1;
226     else
227       return nil;
228   }
229 }
230
231
232 - (char *) getStringResource: (const char *) name
233 {
234   NSObject *o = [self getObjectResource:name];
235   //NSLog(@"%s = %@",name,o);
236   if (o == nil) {
237     if (! (!strcmp(name, "eraseMode") || // erase.c
238            // xlockmore.c reads all of these whether used or not...
239            !strcmp(name, "right3d") ||
240            !strcmp(name, "left3d") ||
241            !strcmp(name, "both3d") ||
242            !strcmp(name, "none3d") ||
243            !strcmp(name, "font") ||
244            !strcmp(name, "labelFont") ||  // grabclient.c
245            !strcmp(name, "titleFont") ||
246            !strcmp(name, "fpsFont") ||    // fps.c
247            !strcmp(name, "foreground") || // fps.c
248            !strcmp(name, "background") ||
249            !strcmp(name, "textLiteral")
250            ))
251       NSLog(@"warning: no preference \"%s\" [string]", name);
252     return NULL;
253   }
254   if (! [o isKindOfClass:[NSString class]]) {
255     NSLog(@"asked for %s as a string, but it is a %@", name, [o class]);
256     o = [(NSNumber *) o stringValue];
257   }
258
259   NSString *os = (NSString *) o;
260   char *result = strdup ([os cStringUsingEncoding:NSUTF8StringEncoding]);
261
262   // Kludge: if the string is surrounded with single-quotes, remove them.
263   // This happens when the .xml file says things like arg="-foo 'bar baz'"
264   if (result[0] == '\'' && result[strlen(result)-1] == '\'') {
265     result[strlen(result)-1] = 0;
266     strcpy (result, result+1);
267   }
268
269   // Kludge: assume that any string that begins with "~" and has a "/"
270   // anywhere in it should be expanded as if it is a pathname.
271   if (result[0] == '~' && strchr (result, '/')) {
272     os = [NSString stringWithCString:result encoding:NSUTF8StringEncoding];
273     free (result);
274     result = strdup ([[os stringByExpandingTildeInPath]
275                        cStringUsingEncoding:NSUTF8StringEncoding]);
276   }
277
278   return result;
279 }
280
281
282 - (double) getFloatResource: (const char *) name
283 {
284   NSObject *o = [self getObjectResource:name];
285   if (o == nil) {
286     // xlockmore.c reads all of these whether used or not...
287     if (! (!strcmp(name, "cycles") ||
288            !strcmp(name, "size") ||
289            !strcmp(name, "use3d") ||
290            !strcmp(name, "delta3d") ||
291            !strcmp(name, "wireframe") ||
292            !strcmp(name, "showFPS") ||
293            !strcmp(name, "fpsSolid") ||
294            !strcmp(name, "fpsTop") ||
295            !strcmp(name, "mono") ||
296            !strcmp(name, "count") ||
297            !strcmp(name, "ncolors") ||
298            !strcmp(name, "doFPS") ||      // fps.c
299            !strcmp(name, "eraseSeconds")  // erase.c
300            ))
301       NSLog(@"warning: no preference \"%s\" [float]", name);
302     return 0.0;
303   }
304   if ([o isKindOfClass:[NSString class]]) {
305     return [(NSString *) o doubleValue];
306   } else if ([o isKindOfClass:[NSNumber class]]) {
307     return [(NSNumber *) o doubleValue];
308   } else {
309     NSAssert2(0, @"%s = \"%@\" but should have been an NSNumber", name, o);
310     abort();
311   }
312 }
313
314
315 - (int) getIntegerResource: (const char *) name
316 {
317   // Sliders might store float values for integral resources; round them.
318   float v = [self getFloatResource:name];
319   int i = (int) (v + (v < 0 ? -0.5 : 0.5)); // ignore sign or -1 rounds to 0
320   // if (i != v) NSLog(@"%s: rounded %.3f to %d", name, v, i);
321   return i;
322 }
323
324
325 - (BOOL) getBooleanResource: (const char *) name
326 {
327   NSObject *o = [self getObjectResource:name];
328   if (! o) {
329     return NO;
330   } else if ([o isKindOfClass:[NSNumber class]]) {
331     double n = [(NSNumber *) o doubleValue];
332     if (n == 0) return NO;
333     else if (n == 1) return YES;
334     else goto FAIL;
335   } else if ([o isKindOfClass:[NSString class]]) {
336     NSString *s = [((NSString *) o) lowercaseString];
337     if ([s isEqualToString:@"true"] ||
338         [s isEqualToString:@"yes"] ||
339         [s isEqualToString:@"1"])
340       return YES;
341     else if ([s isEqualToString:@"false"] ||
342              [s isEqualToString:@"no"] ||
343              [s isEqualToString:@"0"] ||
344              [s isEqualToString:@""])
345       return NO;
346     else
347       goto FAIL;
348   } else {
349   FAIL:
350     NSAssert2(0, @"%s = \"%@\" but should have been a boolean", name, o);
351     abort();
352   }
353 }
354
355
356 - (id) initWithName: (NSString *) name
357             xrmKeys: (const XrmOptionDescRec *) opts
358            defaults: (const char * const *) defs
359 {
360   self = [self init];
361   if (!self) return nil;
362
363 # ifndef USE_IPHONE
364   userDefaults = [ScreenSaverDefaults defaultsForModuleWithName:name];
365 # else  // USE_IPHONE
366   userDefaults = [NSUserDefaults standardUserDefaults];
367 # endif // USE_IPHONE
368
369   // Convert "org.jwz.xscreensaver.NAME" to just "NAME".
370   NSRange r = [name rangeOfString:@"." options:NSBackwardsSearch];
371   if (r.length)
372     name = [name substringFromIndex:r.location+1];
373   name = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
374   saver_name = [name retain];
375
376   [self registerXrmKeys:opts defaults:defs];
377   return self;
378 }
379
380 - (void) dealloc
381 {
382   [saver_name release];
383   [userDefaultsController release];
384   [super dealloc];
385 }
386
387 @end