From http://www.jwz.org/xscreensaver/xscreensaver-5.32.tar.gz
[xscreensaver] / OSX / XScreenSaverConfigSheet.m
1 /* xscreensaver, Copyright (c) 2006-2014 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 /* XScreenSaver uses XML files to describe the user interface for configuring
13    the various screen savers.  These files live in .../hacks/config/ and
14    say relatively high level things like: "there should be a checkbox
15    labelled "Leave Trails", and when it is checked, add the option '-trails'
16    to the command line when launching the program."
17
18    This code reads that XML and constructs a Cocoa interface from it.
19    The Cocoa controls are hooked up to NSUserDefaultsController to save
20    those settings into the MacOS preferences system.  The Cocoa preferences
21    names are the same as the resource names specified in the screenhack's
22    'options' array (we use that array to map the command line switches
23    specified in the XML to the resource names to use).
24  */
25
26 #import "XScreenSaverConfigSheet.h"
27 #import "Updater.h"
28
29 #import "jwxyz.h"
30 #import "InvertedSlider.h"
31
32 #ifdef USE_IPHONE
33 # define NSView      UIView
34 # define NSRect      CGRect
35 # define NSSize      CGSize
36 # define NSTextField UITextField
37 # define NSButton    UIButton
38 # define NSFont      UIFont
39 # define NSStepper   UIStepper
40 # define NSMenuItem  UIMenuItem
41 # define NSText      UILabel
42 # define minValue    minimumValue
43 # define maxValue    maximumValue
44 # define setMinValue setMinimumValue
45 # define setMaxValue setMaximumValue
46 # define LABEL       UILabel
47 #else
48 # define LABEL       NSTextField
49 #endif // USE_IPHONE
50
51 #undef LABEL_ABOVE_SLIDER
52 #define USE_HTML_LABELS
53
54
55 #pragma mark XML Parser
56
57 /* I used to use the "NSXMLDocument" XML parser, but that doesn't exist
58    on iOS.  The "NSXMLParser" parser exists on both OSX and iOS, so I
59    converted to use that.  However, to avoid having to re-write all of
60    the old code, I faked out a halfassed implementation of the
61    "NSXMLNode" class that "NSXMLDocument" used to return.
62  */
63
64 #define NSXMLNode          SimpleXMLNode
65 #define NSXMLElement       SimpleXMLNode
66 #define NSXMLCommentKind   SimpleXMLCommentKind
67 #define NSXMLElementKind   SimpleXMLElementKind
68 #define NSXMLAttributeKind SimpleXMLAttributeKind
69 #define NSXMLTextKind      SimpleXMLTextKind
70
71 typedef enum { SimpleXMLCommentKind,
72                SimpleXMLElementKind,
73                SimpleXMLAttributeKind,
74                SimpleXMLTextKind,
75 } SimpleXMLKind;
76
77 @interface SimpleXMLNode : NSObject
78 {
79   SimpleXMLKind kind;
80   NSString *name;
81   SimpleXMLNode *parent;
82   NSMutableArray *children;
83   NSMutableArray *attributes;
84   id object;
85 }
86
87 @property(nonatomic) SimpleXMLKind kind;
88 @property(nonatomic, retain) NSString *name;
89 @property(nonatomic, retain) SimpleXMLNode *parent;
90 @property(nonatomic, retain) NSMutableArray *children;
91 @property(nonatomic, retain) NSMutableArray *attributes;
92 @property(nonatomic, retain, getter=objectValue, setter=setObjectValue:)
93   id object;
94
95 @end
96
97 @implementation SimpleXMLNode
98
99 @synthesize kind;
100 @synthesize name;
101 //@synthesize parent;
102 @synthesize children;
103 @synthesize attributes;
104 @synthesize object;
105
106 - (id) init
107 {
108   self = [super init];
109   attributes = [NSMutableArray arrayWithCapacity:10];
110   return self;
111 }
112
113
114 - (id) initWithName:(NSString *)n
115 {
116   self = [self init];
117   [self setKind:NSXMLElementKind];
118   [self setName:n];
119   return self;
120 }
121
122
123 - (void) setAttributesAsDictionary:(NSDictionary *)dict
124 {
125   for (NSString *key in dict) {
126     NSObject *val = [dict objectForKey:key];
127     SimpleXMLNode *n = [[SimpleXMLNode alloc] init];
128     [n setKind:SimpleXMLAttributeKind];
129     [n setName:key];
130     [n setObjectValue:val];
131     [attributes addObject:n];
132   }
133 }
134
135 - (SimpleXMLNode *) parent { return parent; }
136
137 - (void) setParent:(SimpleXMLNode *)p
138 {
139   NSAssert (!parent, @"parent already set");
140   if (!p) return;
141   parent = p;
142   NSMutableArray *kids = [p children];
143   if (!kids) {
144     kids = [NSMutableArray arrayWithCapacity:10];
145     [p setChildren:kids];
146   }
147   [kids addObject:self];
148 }
149 @end
150
151
152 #pragma mark Implementing radio buttons
153
154 /* The UIPickerView is a hideous and uncustomizable piece of shit.
155    I can't believe Apple actually released that thing on the world.
156    Let's fake up some radio buttons instead.
157  */
158
159 #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
160
161 @interface RadioButton : UILabel
162 {
163   int index;
164   NSArray *items;
165 }
166
167 @property(nonatomic) int index;
168 @property(nonatomic, retain) NSArray *items;
169
170 @end
171
172 @implementation RadioButton
173
174 @synthesize index;
175 @synthesize items;
176
177 - (id) initWithIndex:(int)_index items:_items
178 {
179   self = [super initWithFrame:CGRectZero];
180   index = _index;
181   items = [_items retain];
182
183   [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
184   [self setBackgroundColor:[UIColor clearColor]];
185   [self sizeToFit];
186
187   return self;
188 }
189
190 @end
191
192
193 # endif // !USE_PICKER_VIEW
194
195
196 # pragma mark Implementing labels with clickable links
197
198 #if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
199
200 @interface HTMLLabel : UIView <UIWebViewDelegate>
201 {
202   NSString *html;
203   UIFont *font;
204   UIWebView *webView;
205 }
206
207 @property(nonatomic, retain) NSString *html;
208 @property(nonatomic, retain) UIWebView *webView;
209
210 - (id) initWithHTML:(NSString *)h font:(UIFont *)f;
211 - (id) initWithText:(NSString *)t font:(UIFont *)f;
212 - (void) setHTML:(NSString *)h;
213 - (void) setText:(NSString *)t;
214 - (void) sizeToFit;
215
216 @end
217
218 @implementation HTMLLabel
219
220 @synthesize html;
221 @synthesize webView;
222
223 - (id) initWithHTML:(NSString *)h font:(UIFont *)f
224 {
225   self = [super init];
226   if (! self) return 0;
227   font = [f retain];
228   webView = [[UIWebView alloc] init];
229   webView.delegate = self;
230   webView.dataDetectorTypes = UIDataDetectorTypeNone;
231   self.   autoresizingMask = UIViewAutoresizingNone;  // we do it manually
232   webView.autoresizingMask = UIViewAutoresizingNone;
233   webView.scrollView.scrollEnabled = NO; 
234   webView.scrollView.bounces = NO;
235   webView.opaque = NO;
236   [webView setBackgroundColor:[UIColor clearColor]];
237
238   [self addSubview: webView];
239   [self setHTML: h];
240   return self;
241 }
242
243 - (id) initWithText:(NSString *)t font:(UIFont *)f
244 {
245   self = [self initWithHTML:@"" font:f];
246   if (! self) return 0;
247   [self setText: t];
248   return self;
249 }
250
251
252 - (void) setHTML: (NSString *)h
253 {
254   if (! h) return;
255   [h retain];
256   if (html) [html release];
257   html = h;
258   NSString *h2 =
259     [NSString stringWithFormat:
260                 @"<!DOCTYPE HTML PUBLIC "
261                    "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
262                    " \"http://www.w3.org/TR/html4/loose.dtd\">"
263                  "<HTML>"
264                   "<HEAD>"
265 //                   "<META NAME=\"viewport\" CONTENT=\""
266 //                      "width=device-width"
267 //                      "initial-scale=1.0;"
268 //                      "maximum-scale=1.0;\">"
269                    "<STYLE>"
270                     "<!--\n"
271                       "body {"
272                       " margin: 0; padding: 0; border: 0;"
273                       " font-family: \"%@\";"
274                       " font-size: %.4fpx;"     // Must be "px", not "pt"!
275                       " line-height: %.4fpx;"   // And no spaces before it.
276                       " -webkit-text-size-adjust: none;"
277                       "}"
278                     "\n//-->\n"
279                    "</STYLE>"
280                   "</HEAD>"
281                   "<BODY>"
282                    "%@"
283                   "</BODY>"
284                  "</HTML>",
285               [font fontName],
286               [font pointSize],
287               [font lineHeight],
288               h];
289   [webView stopLoading];
290   [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
291 }
292
293
294 static char *anchorize (const char *url);
295
296 - (void) setText: (NSString *)t
297 {
298   t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
299                                            whitespaceCharacterSet]];
300   t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&amp;"];
301   t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"];
302   t = [t stringByReplacingOccurrencesOfString:@">" withString:@"&gt;"];
303   t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
304   t = [t stringByReplacingOccurrencesOfString:@"<P>  "
305          withString:@"<P> &nbsp; &nbsp; &nbsp; &nbsp; "];
306   t = [t stringByReplacingOccurrencesOfString:@"\n "
307          withString:@"<BR> &nbsp; &nbsp; &nbsp; &nbsp; "];
308
309   NSString *h = @"";
310   for (NSString *s in
311          [t componentsSeparatedByCharactersInSet:
312               [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
313     if ([s hasPrefix:@"http://"] ||
314         [s hasPrefix:@"https://"]) {
315       char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
316       NSString *a2 = [NSString stringWithCString: anchor
317                                encoding: NSUTF8StringEncoding];
318       s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
319       free (anchor);
320     }
321     h = [NSString stringWithFormat: @"%@ %@", h, s];
322   }
323
324   h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"];
325   h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"];
326   h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
327                                            whitespaceAndNewlineCharacterSet]];
328
329   [self setHTML: h];
330 }
331
332
333 -(BOOL) webView:(UIWebView *)wv
334         shouldStartLoadWithRequest:(NSURLRequest *)req
335         navigationType:(UIWebViewNavigationType)type
336 {
337   // Force clicked links to open in Safari, not in this window.
338   if (type == UIWebViewNavigationTypeLinkClicked) {
339     [[UIApplication sharedApplication] openURL:[req URL]];
340     return NO;
341   }
342   return YES;
343 }
344
345
346 - (void) setFrame: (CGRect)r
347 {
348   [super setFrame: r];
349   r.origin.x = 0;
350   r.origin.y = 0;
351   [webView setFrame: r];
352 }
353
354
355 - (NSString *) stripTags:(NSString *)str
356 {
357   NSString *result = @"";
358
359   // Add newlines.
360   str = [str stringByReplacingOccurrencesOfString:@"<P>"
361              withString:@"<BR><BR>"
362              options:NSCaseInsensitiveSearch
363              range:NSMakeRange(0, [str length])];
364   str = [str stringByReplacingOccurrencesOfString:@"<BR>"
365              withString:@"\n"
366              options:NSCaseInsensitiveSearch
367              range:NSMakeRange(0, [str length])];
368
369   // Remove HREFs.
370   for (NSString *s in [str componentsSeparatedByString: @"<"]) {
371     NSRange r = [s rangeOfString:@">"];
372     if (r.length > 0)
373       s = [s substringFromIndex: r.location + r.length];
374     result = [result stringByAppendingString: s];
375   }
376
377   // Compress internal horizontal whitespace.
378   str = result;
379   result = @"";
380   for (NSString *s in [str componentsSeparatedByCharactersInSet:
381                              [NSCharacterSet whitespaceCharacterSet]]) {
382     if ([result length] == 0)
383       result = s;
384     else if ([s length] > 0)
385       result = [NSString stringWithFormat: @"%@ %@", result, s];
386   }
387
388   return result;
389 }
390
391
392 - (void) sizeToFit
393 {
394   CGRect r = [self frame];
395
396   /* It would be sensible to just ask the UIWebView how tall the page is,
397      instead of hoping that NSString and UIWebView measure fonts and do
398      wrapping in exactly the same way, but since UIWebView is asynchronous,
399      we'd have to wait for the document to load first, e.g.:
400
401        - Start the document loading;
402        - return a default height to use for the UITableViewCell;
403        - wait for the webViewDidFinishLoad delegate method to fire;
404        - then force the UITableView to reload, to pick up the new height.
405
406      But I couldn't make that work.
407    */
408 # if 0
409   r.size.height = [[webView
410                      stringByEvaluatingJavaScriptFromString:
411                        @"document.body.offsetHeight"]
412                     doubleValue];
413 # else
414   NSString *text = [self stripTags: html];
415   CGSize s = r.size;
416   s.height = 999999;
417   s = [text sizeWithFont: font
418             constrainedToSize: s
419             lineBreakMode:NSLineBreakByWordWrapping];
420   r.size.height = s.height;
421 # endif
422
423   [self setFrame: r];
424 }
425
426
427 - (void) dealloc
428 {
429   [html release];
430   [font release];
431   [webView release];
432   [super dealloc];
433 }
434
435 @end
436
437 #endif // USE_IPHONE && USE_HTML_LABELS
438
439
440 @interface XScreenSaverConfigSheet (Private)
441
442 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
443
444 # ifndef USE_IPHONE
445 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
446 - (void) placeChild: (NSView *)c on:(NSView *)p;
447 static NSView *last_child (NSView *parent);
448 static void layout_group (NSView *group, BOOL horiz_p);
449 # else // USE_IPHONE
450 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
451 - (void) placeChild: (NSObject *)c on:(NSView *)p;
452 - (void) placeSeparator;
453 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
454 - (void) refreshTableView;
455 # endif // USE_IPHONE
456
457 @end
458
459
460 @implementation XScreenSaverConfigSheet
461
462 # define LEFT_MARGIN      20   // left edge of window
463 # define COLUMN_SPACING   10   // gap between e.g. labels and text fields
464 # define LEFT_LABEL_WIDTH 70   // width of all left labels
465 # define LINE_SPACING     10   // leading between each line
466
467 # define FONT_SIZE        17   // Magic hardcoded UITableView font size.
468
469 #pragma mark Talking to the resource database
470
471
472 /* Normally we read resources by looking up "KEY" in the database
473    "org.jwz.xscreensaver.SAVERNAME".  But in the all-in-one iPhone
474    app, everything is stored in the database "org.jwz.xscreensaver"
475    instead, so transform keys to "SAVERNAME.KEY".
476
477    NOTE: This is duplicated in PrefsReader.m, cause I suck.
478  */
479 - (NSString *) makeKey:(NSString *)key
480 {
481 # ifdef USE_IPHONE
482   NSString *prefix = [saver_name stringByAppendingString:@"."];
483   if (! [key hasPrefix:prefix])  // Don't double up!
484     key = [prefix stringByAppendingString:key];
485 # endif
486   return key;
487 }
488
489
490 - (NSString *) makeCKey:(const char *)key
491 {
492   return [self makeKey:[NSString stringWithCString:key
493                                  encoding:NSUTF8StringEncoding]];
494 }
495
496
497 /* Given a command-line option, returns the corresponding resource name.
498    Any arguments in the switch string are ignored (e.g., "-foo x").
499  */
500 - (NSString *) switchToResource:(NSString *)cmdline_switch
501                            opts:(const XrmOptionDescRec *)opts_array
502                          valRet:(NSString **)val_ret
503 {
504   char buf[255];
505   char *tail = 0;
506   NSAssert(cmdline_switch, @"cmdline switch is null");
507   if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
508                           encoding:NSUTF8StringEncoding]) {
509     NSAssert1(0, @"unable to convert %@", cmdline_switch);
510     return 0;
511   }
512   char *s = strpbrk(buf, " \t\r\n");
513   if (s && *s) {
514     *s = 0;
515     tail = s+1;
516     while (*tail && (*tail == ' ' || *tail == '\t'))
517       tail++;
518   }
519   
520   while (opts_array[0].option) {
521     if (!strcmp (opts_array[0].option, buf)) {
522       const char *ret = 0;
523
524       if (opts_array[0].argKind == XrmoptionNoArg) {
525         if (tail && *tail)
526           NSAssert1 (0, @"expected no args to switch: \"%@\"",
527                      cmdline_switch);
528         ret = opts_array[0].value;
529       } else {
530         if (!tail || !*tail)
531           NSAssert1 (0, @"expected args to switch: \"%@\"",
532                      cmdline_switch);
533         ret = tail;
534       }
535
536       if (val_ret)
537         *val_ret = (ret
538                     ? [NSString stringWithCString:ret
539                                          encoding:NSUTF8StringEncoding]
540                     : 0);
541       
542       const char *res = opts_array[0].specifier;
543       while (*res && (*res == '.' || *res == '*'))
544         res++;
545       return [self makeCKey:res];
546     }
547     opts_array++;
548   }
549   
550   NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
551   return 0;
552 }
553
554
555 - (NSUserDefaultsController *)controllerForKey:(NSString *)key
556 {
557   static NSDictionary *a = 0;
558   if (! a) {
559     a = UPDATER_DEFAULTS;
560     [a retain];
561   }
562   if ([a objectForKey:key])
563     // These preferences are global to all xscreensavers.
564     return globalDefaultsController;
565   else
566     // All other preferences are per-saver.
567     return userDefaultsController;
568 }
569
570
571 #ifdef USE_IPHONE
572
573 // Called when a slider is bonked.
574 //
575 - (void)sliderAction:(UISlider*)sender
576 {
577   if ([active_text_field canResignFirstResponder])
578     [active_text_field resignFirstResponder];
579   NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
580
581   // Hacky API. See comment in InvertedSlider.m.
582   double v = ([sender isKindOfClass: [InvertedSlider class]]
583               ? [(InvertedSlider *) sender transformedValue]
584               : [sender value]);
585
586   [[self controllerForKey:pref_key]
587     setObject:((v == (int) v)
588                ? [NSNumber numberWithInt:(int) v]
589                : [NSNumber numberWithDouble: v])
590     forKey:pref_key];
591 }
592
593 // Called when a checkbox/switch is bonked.
594 //
595 - (void)switchAction:(UISwitch*)sender
596 {
597   if ([active_text_field canResignFirstResponder])
598     [active_text_field resignFirstResponder];
599   NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
600   NSString *v = ([sender isOn] ? @"true" : @"false");
601   [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
602 }
603
604 # ifdef USE_PICKER_VIEW
605 // Called when a picker is bonked.
606 //
607 - (void)pickerView:(UIPickerView *)pv
608         didSelectRow:(NSInteger)row
609         inComponent:(NSInteger)column
610 {
611   if ([active_text_field canResignFirstResponder])
612     [active_text_field resignFirstResponder];
613
614   NSAssert (column == 0, @"internal error");
615   NSArray *a = [picker_values objectAtIndex: [pv tag]];
616   if (! a) return;  // Too early?
617   a = [a objectAtIndex:row];
618   NSAssert (a, @"missing row");
619
620 //NSString *label    = [a objectAtIndex:0];
621   NSString *pref_key = [a objectAtIndex:1];
622   NSObject *pref_val = [a objectAtIndex:2];
623   [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
624 }
625 # else  // !USE_PICKER_VIEW
626
627 // Called when a RadioButton is bonked.
628 //
629 - (void)radioAction:(RadioButton*)sender
630 {
631   if ([active_text_field canResignFirstResponder])
632     [active_text_field resignFirstResponder];
633
634   NSArray *item = [[sender items] objectAtIndex: [sender index]];
635   NSString *pref_key = [item objectAtIndex:1];
636   NSObject *pref_val = [item objectAtIndex:2];
637   [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
638 }
639
640 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
641 {
642   active_text_field = tf;
643   return YES;
644 }
645
646 - (void)textFieldDidEndEditing:(UITextField *)tf
647 {
648   NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
649   NSString *txt = [tf text];
650   [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
651 }
652
653 - (BOOL)textFieldShouldReturn:(UITextField *)tf
654 {
655   active_text_field = nil;
656   [tf resignFirstResponder];
657   return YES;
658 }
659
660 # endif // !USE_PICKER_VIEW
661
662 #endif // USE_IPHONE
663
664
665 # ifndef USE_IPHONE
666
667 - (void) okAction:(NSObject *)arg
668 {
669   [userDefaultsController   commitEditing];
670   [globalDefaultsController commitEditing];
671   [userDefaultsController   save:self];
672   [globalDefaultsController save:self];
673   [NSApp endSheet:self returnCode:NSOKButton];
674   [self close];
675 }
676
677 - (void) cancelAction:(NSObject *)arg
678 {
679   [userDefaultsController   revert:self];
680   [globalDefaultsController revert:self];
681   [NSApp endSheet:self returnCode:NSCancelButton];
682   [self close];
683 }
684 # endif // !USE_IPHONE
685
686
687 - (void) resetAction:(NSObject *)arg
688 {
689 # ifndef USE_IPHONE
690   [userDefaultsController   revertToInitialValues:self];
691   [globalDefaultsController revertToInitialValues:self];
692 # else  // USE_IPHONE
693
694   for (NSString *key in defaultOptions) {
695     NSObject *val = [defaultOptions objectForKey:key];
696     [[self controllerForKey:key] setObject:val forKey:key];
697   }
698
699   for (UIControl *ctl in pref_ctls) {
700     NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
701     [self bindResource:ctl key:pref_key reload:YES];
702   }
703
704   [self refreshTableView];
705 # endif // USE_IPHONE
706 }
707
708
709 /* Connects a control (checkbox, etc) to the corresponding preferences key.
710  */
711 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
712          reload:(BOOL)reload_p
713 {
714   NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
715 # ifndef USE_IPHONE
716   NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
717                       ? @"selectedObject"
718                       : ([control isKindOfClass:[NSMatrix class]]
719                          ? @"selectedIndex"
720                          : @"value"));
721   [control bind:bindto
722        toObject:prefs
723     withKeyPath:[@"values." stringByAppendingString: pref_key]
724         options:nil];
725 # else  // USE_IPHONE
726   SEL sel;
727   NSObject *val = [prefs objectForKey:pref_key];
728   NSString *sval = 0;
729   double dval = 0;
730
731   if ([val isKindOfClass:[NSString class]]) {
732     sval = (NSString *) val;
733     if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
734         NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
735         NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
736       dval = 1;
737     else
738       dval = [sval doubleValue];
739   } else if ([val isKindOfClass:[NSNumber class]]) {
740     // NSBoolean (__NSCFBoolean) is really NSNumber.
741     dval = [(NSNumber *) val doubleValue];
742     sval = [(NSNumber *) val stringValue];
743   }
744
745   if ([control isKindOfClass:[UISlider class]]) {
746     sel = @selector(sliderAction:);
747     // Hacky API. See comment in InvertedSlider.m.
748     if ([control isKindOfClass:[InvertedSlider class]])
749       [(InvertedSlider *) control setTransformedValue: dval];
750     else
751       [(UISlider *) control setValue: dval];
752   } else if ([control isKindOfClass:[UISwitch class]]) {
753     sel = @selector(switchAction:);
754     [(UISwitch *) control setOn: ((int) dval != 0)];
755 # ifdef USE_PICKER_VIEW
756   } else if ([control isKindOfClass:[UIPickerView class]]) {
757     sel = 0;
758     [(UIPickerView *) control selectRow:((int)dval) inComponent:0
759                       animated:NO];
760 # else  // !USE_PICKER_VIEW
761   } else if ([control isKindOfClass:[RadioButton class]]) {
762     sel = 0;  // radioAction: sent from didSelectRowAtIndexPath.
763   } else if ([control isKindOfClass:[UITextField class]]) {
764     sel = 0;  // ####
765     [(UITextField *) control setText: sval];
766 # endif // !USE_PICKER_VIEW
767   } else {
768     NSAssert (0, @"unknown class");
769   }
770
771   // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
772
773   if (!reload_p) {
774     if (! pref_keys) {
775       pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
776       pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
777     }
778
779     [pref_keys addObject: [self makeKey:pref_key]];
780     [pref_ctls addObject: control];
781     ((UIControl *) control).tag = [pref_keys count] - 1;
782
783     if (sel) {
784       [(UIControl *) control addTarget:self action:sel
785                      forControlEvents:UIControlEventValueChanged];
786     }
787   }
788
789 # endif // USE_IPHONE
790
791 # if 0
792   NSObject *def = [[prefs defaults] objectForKey:pref_key];
793   NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
794   s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
795   s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
796   s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
797   NSLog (@"%@ %@/%@", s, [def class], [control class]);
798 # endif
799 }
800
801
802 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
803 {
804   [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
805 }
806
807
808
809 - (void) bindSwitch:(NSObject *)control
810             cmdline:(NSString *)cmd
811 {
812   [self bindResource:control 
813         key:[self switchToResource:cmd opts:opts valRet:0]];
814 }
815
816
817 #pragma mark Text-manipulating utilities
818
819
820 static NSString *
821 unwrap (NSString *text)
822 {
823   // Unwrap lines: delete \n but do not delete \n\n.
824   //
825   NSArray *lines = [text componentsSeparatedByString:@"\n"];
826   NSUInteger i, nlines = [lines count];
827   BOOL eolp = YES;
828
829   text = @"\n";      // start with one blank line
830
831   // skip trailing blank lines in file
832   for (i = nlines-1; i > 0; i--) {
833     NSString *s = (NSString *) [lines objectAtIndex:i];
834     if ([s length] > 0)
835       break;
836     nlines--;
837   }
838
839   // skip leading blank lines in file
840   for (i = 0; i < nlines; i++) {
841     NSString *s = (NSString *) [lines objectAtIndex:i];
842     if ([s length] > 0)
843       break;
844   }
845
846   // unwrap
847   Bool any = NO;
848   for (; i < nlines; i++) {
849     NSString *s = (NSString *) [lines objectAtIndex:i];
850     if ([s length] == 0) {
851       text = [text stringByAppendingString:@"\n\n"];
852       eolp = YES;
853     } else if ([s characterAtIndex:0] == ' ' ||
854                [s hasPrefix:@"Copyright "] ||
855                [s hasPrefix:@"http://"]) {
856       // don't unwrap if the following line begins with whitespace,
857       // or with the word "Copyright", or if it begins with a URL.
858       if (any && !eolp)
859         text = [text stringByAppendingString:@"\n"];
860       text = [text stringByAppendingString:s];
861       any = YES;
862       eolp = NO;
863     } else {
864       if (!eolp)
865         text = [text stringByAppendingString:@" "];
866       text = [text stringByAppendingString:s];
867       eolp = NO;
868       any = YES;
869     }
870   }
871
872   return text;
873 }
874
875
876 # ifndef USE_IPHONE
877 /* Makes the text up to the first comma be bold.
878  */
879 static void
880 boldify (NSText *nstext)
881 {
882   NSString *text = [nstext string];
883   NSRange r = [text rangeOfString:@"," options:0];
884   r.length = r.location+1;
885
886   r.location = 0;
887
888   NSFont *font = [nstext font];
889   font = [NSFont boldSystemFontOfSize:[font pointSize]];
890   [nstext setFont:font range:r];
891 }
892 # endif // !USE_IPHONE
893
894
895 /* Creates a human-readable anchor to put on a URL.
896  */
897 static char *
898 anchorize (const char *url)
899 {
900   const char *wiki = "http://en.wikipedia.org/wiki/";
901   const char *math = "http://mathworld.wolfram.com/";
902   if (!strncmp (wiki, url, strlen(wiki))) {
903     char *anchor = (char *) malloc (strlen(url) * 3 + 10);
904     strcpy (anchor, "Wikipedia: \"");
905     const char *in = url + strlen(wiki);
906     char *out = anchor + strlen(anchor);
907     while (*in) {
908       if (*in == '_') {
909         *out++ = ' ';
910       } else if (*in == '#') {
911         *out++ = ':';
912         *out++ = ' ';
913       } else if (*in == '%') {
914         char hex[3];
915         hex[0] = in[1];
916         hex[1] = in[2];
917         hex[2] = 0;
918         int n = 0;
919         sscanf (hex, "%x", &n);
920         *out++ = (char) n;
921         in += 2;
922       } else {
923         *out++ = *in;
924       }
925       in++;
926     }
927     *out++ = '"';
928     *out = 0;
929     return anchor;
930
931   } else if (!strncmp (math, url, strlen(math))) {
932     char *anchor = (char *) malloc (strlen(url) * 3 + 10);
933     strcpy (anchor, "MathWorld: \"");
934     const char *start = url + strlen(wiki);
935     const char *in = start;
936     char *out = anchor + strlen(anchor);
937     while (*in) {
938       if (*in == '_') {
939         *out++ = ' ';
940       } else if (in != start && *in >= 'A' && *in <= 'Z') {
941         *out++ = ' ';
942         *out++ = *in;
943       } else if (!strncmp (in, ".htm", 4)) {
944         break;
945       } else {
946         *out++ = *in;
947       }
948       in++;
949     }
950     *out++ = '"';
951     *out = 0;
952     return anchor;
953
954   } else {
955     return strdup (url);
956   }
957 }
958
959
960 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
961
962 /* Converts any http: URLs in the given text field to clickable links.
963  */
964 static void
965 hreffify (NSText *nstext)
966 {
967 # ifndef USE_IPHONE
968   NSString *text = [nstext string];
969   [nstext setRichText:YES];
970 # else
971   NSString *text = [nstext text];
972 # endif
973
974   int L = [text length];
975   NSRange start;                // range is start-of-search to end-of-string
976   start.location = 0;
977   start.length = L;
978   while (start.location < L) {
979
980     // Find the beginning of a URL...
981     //
982     NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
983     if (r2.location == NSNotFound)
984       break;
985
986     // Next time around, start searching after this.
987     start.location = r2.location + r2.length;
988     start.length = L - start.location;
989
990     // Find the end of a URL (whitespace or EOF)...
991     //
992     NSRange r3 = [text rangeOfCharacterFromSet:
993                          [NSCharacterSet whitespaceAndNewlineCharacterSet]
994                        options:0 range:start];
995     if (r3.location == NSNotFound)    // EOF
996       r3.location = L, r3.length = 0;
997
998     // Next time around, start searching after this.
999     start.location = r3.location;
1000     start.length = L - start.location;
1001
1002     // Set r2 to the start/length of this URL.
1003     r2.length = start.location - r2.location;
1004
1005     // Extract the URL.
1006     NSString *nsurl = [text substringWithRange:r2];
1007     const char *url = [nsurl UTF8String];
1008
1009     // If this is a Wikipedia URL, make the linked text be prettier.
1010     //
1011     char *anchor = anchorize(url);
1012
1013 # ifndef USE_IPHONE
1014
1015     // Construct the RTF corresponding to <A HREF="url">anchor</A>
1016     //
1017     const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1018     char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1019     sprintf (rtf, fmt, url, anchor);
1020
1021     NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1022     [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1023
1024 # else  // !USE_IPHONE
1025     // *anchor = 0; // Omit Wikipedia anchor 
1026     text = [text stringByReplacingCharactersInRange:r2
1027                  withString:[NSString stringWithCString:anchor
1028                                       encoding:NSUTF8StringEncoding]];
1029     // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1030     //              withString:@"\n\n"];
1031 # endif // !USE_IPHONE
1032
1033     free (anchor);
1034
1035     int L2 = [text length];  // might have changed
1036     start.location -= (L - L2);
1037     L = L2;
1038   }
1039
1040 # ifdef USE_IPHONE
1041   [nstext setText:text];
1042   [nstext sizeToFit];
1043 # endif
1044 }
1045
1046 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1047
1048
1049
1050 #pragma mark Creating controls from XML
1051
1052
1053 /* Parse the attributes of an XML tag into a dictionary.
1054    For input, the dictionary should have as attributes the keys, each
1055    with @"" as their value.
1056    On output, the dictionary will set the keys to the values specified,
1057    and keys that were not specified will not be present in the dictionary.
1058    Warnings are printed if there are duplicate or unknown attributes.
1059  */
1060 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1061 {
1062   NSArray *attrs = [(NSXMLElement *) node attributes];
1063   NSUInteger n = [attrs count];
1064   int i;
1065   
1066   // For each key in the dictionary, fill in the dict with the corresponding
1067   // value.  The value @"" is assumed to mean "un-set".  Issue a warning if
1068   // an attribute is specified twice.
1069   //
1070   for (i = 0; i < n; i++) {
1071     NSXMLNode *attr = [attrs objectAtIndex:i];
1072     NSString *key = [attr name];
1073     NSString *val = [attr objectValue];
1074     NSString *old = [dict objectForKey:key];
1075     
1076     if (! old) {
1077       NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1078     } else if ([old length] != 0) {
1079       NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1080     } else {
1081       [dict setValue:val forKey:key];
1082     }
1083   }
1084   
1085   // Remove from the dictionary any keys whose value is still @"", 
1086   // meaning there was no such attribute specified.
1087   //
1088   NSArray *keys = [dict allKeys];
1089   n = [keys count];
1090   for (i = 0; i < n; i++) {
1091     NSString *key = [keys objectAtIndex:i];
1092     NSString *val = [dict objectForKey:key];
1093     if ([val length] == 0)
1094       [dict removeObjectForKey:key];
1095   }
1096
1097 # ifdef USE_IPHONE
1098   // Kludge for starwars.xml:
1099   // If there is a "_low-label" and no "_label", but "_low-label" contains
1100   // spaces, divide them.
1101   NSString *lab = [dict objectForKey:@"_label"];
1102   NSString *low = [dict objectForKey:@"_low-label"];
1103   if (low && !lab) {
1104     NSArray *split =
1105       [[[low stringByTrimmingCharactersInSet:
1106                [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1107          componentsSeparatedByString: @"  "]
1108         filteredArrayUsingPredicate:
1109           [NSPredicate predicateWithFormat:@"length > 0"]];
1110     if (split && [split count] == 2) {
1111       [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1112       [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1113     }
1114   }
1115 # endif // USE_IPHONE
1116 }
1117
1118
1119 /* Handle the options on the top level <xscreensaver> tag.
1120  */
1121 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1122 {
1123   NSMutableDictionary *dict = [@{ @"name":   @"",
1124                                   @"_label": @"",
1125                                   @"gl":     @"" }
1126                                 mutableCopy];
1127   [self parseAttrs:dict node:node];
1128   NSString *name  = [dict objectForKey:@"name"];
1129   NSString *label = [dict objectForKey:@"_label"];
1130     
1131   NSAssert1 (label, @"no _label in %@", [node name]);
1132   NSAssert1 (name, @"no name in \"%@\"", label);
1133   return label;
1134 }
1135
1136
1137 /* Creates a label: an un-editable NSTextField displaying the given text.
1138  */
1139 - (LABEL *) makeLabel:(NSString *)text
1140 {
1141   NSRect rect;
1142   rect.origin.x = rect.origin.y = 0;
1143   rect.size.width = rect.size.height = 10;
1144 # ifndef USE_IPHONE
1145   NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1146   [lab setSelectable:NO];
1147   [lab setEditable:NO];
1148   [lab setBezeled:NO];
1149   [lab setDrawsBackground:NO];
1150   [lab setStringValue:text];
1151   [lab sizeToFit];
1152 # else  // USE_IPHONE
1153   UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1154   [lab setText: [text stringByTrimmingCharactersInSet:
1155                  [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1156   [lab setBackgroundColor:[UIColor clearColor]];
1157   [lab setNumberOfLines:0]; // unlimited
1158   // [lab setLineBreakMode:UILineBreakModeWordWrap];
1159   [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1160   [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1161                              UIViewAutoresizingFlexibleHeight)];
1162 # endif // USE_IPHONE
1163   return lab;
1164 }
1165
1166
1167 /* Creates the checkbox (NSButton) described by the given XML node.
1168  */
1169 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1170 {
1171   NSMutableDictionary *dict = [@{ @"id":       @"",
1172                                   @"_label":    @"",
1173                                   @"arg-set":   @"",
1174                                   @"arg-unset": @"" }
1175                                 mutableCopy];
1176   [self parseAttrs:dict node:node];
1177   NSString *label     = [dict objectForKey:@"_label"];
1178   NSString *arg_set   = [dict objectForKey:@"arg-set"];
1179   NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1180   
1181   if (!label) {
1182     NSAssert1 (0, @"no _label in %@", [node name]);
1183     return;
1184   }
1185   if (!arg_set && !arg_unset) {
1186     NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"", 
1187                label);
1188   }
1189   if (arg_set && arg_unset) {
1190     NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"", 
1191                label);
1192   }
1193   
1194   // sanity-check the choice of argument names.
1195   //
1196   if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1197                   [arg_set hasPrefix:@"--no-"]))
1198     NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1199            label, arg_set);
1200   if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1201                     ![arg_unset hasPrefix:@"--no-"]))
1202     NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1203           label, arg_unset);
1204     
1205   NSRect rect;
1206   rect.origin.x = rect.origin.y = 0;
1207   rect.size.width = rect.size.height = 10;
1208
1209 # ifndef USE_IPHONE
1210
1211   NSButton *button = [[NSButton alloc] initWithFrame:rect];
1212   [button setButtonType:NSSwitchButton];
1213   [button setTitle:label];
1214   [button sizeToFit];
1215   [self placeChild:button on:parent];
1216
1217 # else  // USE_IPHONE
1218
1219   LABEL *lab = [self makeLabel:label];
1220   [self placeChild:lab on:parent];
1221   UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1222   [self placeChild:button on:parent right:YES];
1223   [lab release];
1224
1225 # endif // USE_IPHONE
1226   
1227   [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1228   [button release];
1229 }
1230
1231
1232 /* Creates the number selection control described by the given XML node.
1233    If "type=slider", it's an NSSlider.
1234    If "type=spinbutton", it's a text field with up/down arrows next to it.
1235  */
1236 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1237 {
1238   NSMutableDictionary *dict = [@{ @"id":          @"",
1239                                   @"_label":      @"",
1240                                   @"_low-label":  @"",
1241                                   @"_high-label": @"",
1242                                   @"type":        @"",
1243                                   @"arg":         @"",
1244                                   @"low":         @"",
1245                                   @"high":        @"",
1246                                   @"default":     @"",
1247                                   @"convert":     @"" }
1248                                 mutableCopy];
1249   [self parseAttrs:dict node:node];
1250   NSString *label      = [dict objectForKey:@"_label"];
1251   NSString *low_label  = [dict objectForKey:@"_low-label"];
1252   NSString *high_label = [dict objectForKey:@"_high-label"];
1253   NSString *type       = [dict objectForKey:@"type"];
1254   NSString *arg        = [dict objectForKey:@"arg"];
1255   NSString *low        = [dict objectForKey:@"low"];
1256   NSString *high       = [dict objectForKey:@"high"];
1257   NSString *def        = [dict objectForKey:@"default"];
1258   NSString *cvt        = [dict objectForKey:@"convert"];
1259   
1260   NSAssert1 (arg,  @"no arg in %@", label);
1261   NSAssert1 (type, @"no type in %@", label);
1262
1263   if (! low) {
1264     NSAssert1 (0, @"no low in %@", [node name]);
1265     return;
1266   }
1267   if (! high) {
1268     NSAssert1 (0, @"no high in %@", [node name]);
1269     return;
1270   }
1271   if (! def) {
1272     NSAssert1 (0, @"no default in %@", [node name]);
1273     return;
1274   }
1275   if (cvt && ![cvt isEqualToString:@"invert"]) {
1276     NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1277                label);
1278   }
1279     
1280   // If either the min or max field contains a decimal point, then this
1281   // option may have a floating point value; otherwise, it is constrained
1282   // to be an integer.
1283   //
1284   NSCharacterSet *dot =
1285     [NSCharacterSet characterSetWithCharactersInString:@"."];
1286   BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1287                   [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1288
1289   if ([type isEqualToString:@"slider"]
1290 # ifdef USE_IPHONE  // On iPhone, we use sliders for all numeric values.
1291       || [type isEqualToString:@"spinbutton"]
1292 # endif
1293       ) {
1294
1295     NSRect rect;
1296     rect.origin.x = rect.origin.y = 0;    
1297     rect.size.width = 150;
1298     rect.size.height = 23;  // apparent min height for slider with ticks...
1299     NSSlider *slider;
1300     slider = [[InvertedSlider alloc] initWithFrame:rect
1301                                      inverted: !!cvt
1302                                      integers: !float_p];
1303     [slider setMaxValue:[high doubleValue]];
1304     [slider setMinValue:[low  doubleValue]];
1305     
1306     int range = [slider maxValue] - [slider minValue] + 1;
1307     int range2 = range;
1308     int max_ticks = 21;
1309     while (range2 > max_ticks)
1310       range2 /= 10;
1311
1312     // If we have elided ticks, leave it at the max number of ticks.
1313     if (range != range2 && range2 < max_ticks)
1314       range2 = max_ticks;
1315
1316     // If it's a float, always display the max number of ticks.
1317     if (float_p && range2 < max_ticks)
1318       range2 = max_ticks;
1319
1320 # ifndef USE_IPHONE
1321     [slider setNumberOfTickMarks:range2];
1322
1323     [slider setAllowsTickMarkValuesOnly:
1324               (range == range2 &&  // we are showing the actual number of ticks
1325                !float_p)];         // and we want integer results
1326 # endif // !USE_IPHONE
1327
1328     // #### Note: when the slider's range is large enough that we aren't
1329     //      showing all possible ticks, the slider's value is not constrained
1330     //      to be an integer, even though it should be...
1331     //      Maybe we need to use a value converter or something?
1332
1333     LABEL *lab;
1334     if (label) {
1335       lab = [self makeLabel:label];
1336       [self placeChild:lab on:parent];
1337 # ifdef USE_IPHONE
1338       if (low_label) {
1339         CGFloat s = [NSFont systemFontSize] + 4;
1340         [lab setFont:[NSFont boldSystemFontOfSize:s]];
1341       }
1342 # endif
1343       [lab release];
1344     }
1345     
1346     if (low_label) {
1347       lab = [self makeLabel:low_label];
1348       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
1349 # ifndef USE_IPHONE
1350       [lab setAlignment:1];  // right aligned
1351       rect = [lab frame];
1352       if (rect.size.width < LEFT_LABEL_WIDTH)
1353         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
1354       rect.size.height = [slider frame].size.height;
1355       [lab setFrame:rect];
1356       [self placeChild:lab on:parent];
1357 # else  // USE_IPHONE
1358       [lab setTextAlignment: NSTextAlignmentRight];
1359       // Sometimes rotation screws up truncation.
1360       [lab setLineBreakMode:NSLineBreakByClipping];
1361       [self placeChild:lab on:parent right:(label ? YES : NO)];
1362 # endif // USE_IPHONE
1363
1364       [lab release];
1365      }
1366     
1367 # ifndef USE_IPHONE
1368     [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1369 # else  // USE_IPHONE
1370     [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1371 # endif // USE_IPHONE
1372     
1373     if (low_label) {
1374       // Make left label be same height as slider.
1375       rect = [lab frame];
1376       rect.size.height = [slider frame].size.height;
1377       [lab setFrame:rect];
1378     }
1379
1380     if (! low_label) {
1381       rect = [slider frame];
1382       if (rect.origin.x < LEFT_LABEL_WIDTH)
1383         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1384       [slider setFrame:rect];
1385     }
1386         
1387     if (high_label) {
1388       lab = [self makeLabel:high_label];
1389       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1390       rect = [lab frame];
1391
1392       // Make right label be same height as slider.
1393       rect.size.height = [slider frame].size.height;
1394       [lab setFrame:rect];
1395 # ifdef USE_IPHONE
1396       // Sometimes rotation screws up truncation.
1397       [lab setLineBreakMode:NSLineBreakByClipping];
1398 # endif
1399       [self placeChild:lab on:parent right:YES];
1400       [lab release];
1401      }
1402
1403     [self bindSwitch:slider cmdline:arg];
1404     [slider release];
1405     
1406 #ifndef USE_IPHONE  // On iPhone, we use sliders for all numeric values.
1407
1408   } else if ([type isEqualToString:@"spinbutton"]) {
1409
1410     if (! label) {
1411       NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1412       return;
1413     }
1414     NSAssert1 (!low_label,
1415               @"low-label not allowed in spinbutton \"%@\"", [node name]);
1416     NSAssert1 (!high_label,
1417                @"high-label not allowed in spinbutton \"%@\"", [node name]);
1418     NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1419                [node name]);
1420     
1421     NSRect rect;
1422     rect.origin.x = rect.origin.y = 0;    
1423     rect.size.width = rect.size.height = 10;
1424     
1425     NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1426     [txt setStringValue:@"0000.0"];
1427     [txt sizeToFit];
1428     [txt setStringValue:@""];
1429     
1430     if (label) {
1431       LABEL *lab = [self makeLabel:label];
1432       //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1433       [lab setAlignment:1];  // right aligned
1434       rect = [lab frame];
1435       if (rect.size.width < LEFT_LABEL_WIDTH)
1436         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
1437       rect.size.height = [txt frame].size.height;
1438       [lab setFrame:rect];
1439       [self placeChild:lab on:parent];
1440       [lab release];
1441      }
1442     
1443     [self placeChild:txt on:parent right:(label ? YES : NO)];
1444     
1445     if (! label) {
1446       rect = [txt frame];
1447       if (rect.origin.x < LEFT_LABEL_WIDTH)
1448         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1449       [txt setFrame:rect];
1450     }
1451     
1452     rect.size.width = rect.size.height = 10;
1453     NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1454     [step sizeToFit];
1455     [self placeChild:step on:parent right:YES];
1456     rect = [step frame];
1457     rect.origin.x -= COLUMN_SPACING;  // this one goes close
1458     rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1459     [step setFrame:rect];
1460     
1461     [step setMinValue:[low  doubleValue]];
1462     [step setMaxValue:[high doubleValue]];
1463     [step setAutorepeat:YES];
1464     [step setValueWraps:NO];
1465     
1466     double range = [high doubleValue] - [low doubleValue];
1467     if (range < 1.0)
1468       [step setIncrement:range / 10.0];
1469     else if (range >= 500)
1470       [step setIncrement:range / 100.0];
1471     else
1472       [step setIncrement:1.0];
1473
1474     NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1475     [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1476     [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1477     [fmt setMinimum:[NSNumber numberWithDouble:[low  doubleValue]]];
1478     [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1479     [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1480     [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1481
1482     [fmt setGeneratesDecimalNumbers:float_p];
1483     [[txt cell] setFormatter:fmt];
1484
1485     [self bindSwitch:step cmdline:arg];
1486     [self bindSwitch:txt  cmdline:arg];
1487     
1488     [step release];
1489     [txt release];
1490
1491 # endif // USE_IPHONE
1492
1493   } else {
1494     NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1495   }
1496 }
1497
1498
1499 # ifndef USE_IPHONE
1500 static void
1501 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1502 {
1503   /* If the object associated with this menu item looks like a boolean,
1504      store an NSNumber instead of an NSString, since that's what
1505      will be in the preferences (due to similar logic in PrefsReader).
1506    */
1507   if ([obj isKindOfClass:[NSString class]]) {
1508     NSString *string = (NSString *) obj;
1509     if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1510         NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1511       obj = [NSNumber numberWithBool:YES];
1512     else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1513              NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1514       obj = [NSNumber numberWithBool:NO];
1515     else
1516       obj = string;
1517   }
1518
1519   [item setRepresentedObject:obj];
1520   //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1521 }
1522 # endif // !USE_IPHONE
1523
1524
1525 /* Creates the popup menu described by the given XML node (and its children).
1526  */
1527 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1528 {
1529   NSArray *children = [node children];
1530   NSUInteger i, count = [children count];
1531
1532   if (count <= 0) {
1533     NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1534     return;
1535   }
1536
1537   // get the "id" attribute off the <select> tag.
1538   //
1539   NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1540   [self parseAttrs:dict node:node];
1541   
1542   NSRect rect;
1543   rect.origin.x = rect.origin.y = 0;
1544   rect.size.width = 10;
1545   rect.size.height = 10;
1546
1547   NSString *menu_key = nil;   // the resource key used by items in this menu
1548
1549 # ifndef USE_IPHONE
1550   // #### "Build and Analyze" says that all of our widgets leak, because it
1551   //      seems to not realize that placeChild -> addSubview retains them.
1552   //      Not sure what to do to make these warnings go away.
1553
1554   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1555                                                      pullsDown:NO];
1556   NSMenuItem *def_item = nil;
1557   float max_width = 0;
1558
1559 # else  // USE_IPHONE
1560
1561   NSString *def_item = nil;
1562
1563   rect.size.width  = 0;
1564   rect.size.height = 0;
1565 #  ifdef USE_PICKER_VIEW
1566   UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1567   popup.delegate = self;
1568   popup.dataSource = self;
1569 #  endif // !USE_PICKER_VIEW
1570   NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1571
1572 # endif // USE_IPHONE
1573   
1574   for (i = 0; i < count; i++) {
1575     NSXMLNode *child = [children objectAtIndex:i];
1576
1577     if ([child kind] == NSXMLCommentKind)
1578       continue;
1579     if ([child kind] != NSXMLElementKind) {
1580 //    NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1581       continue;
1582     }
1583
1584     // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1585     //
1586     NSMutableDictionary *dict2 = [@{ @"id":      @"",
1587                                      @"_label":  @"",
1588                                      @"arg-set": @"" }
1589                                    mutableCopy];
1590     [self parseAttrs:dict2 node:child];
1591     NSString *label   = [dict2 objectForKey:@"_label"];
1592     NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1593     
1594     if (!label) {
1595       NSAssert1 (0, @"no _label in %@", [child name]);
1596       continue;
1597     }
1598
1599 # ifndef USE_IPHONE
1600     // create the menu item (and then get a pointer to it)
1601     [popup addItemWithTitle:label];
1602     NSMenuItem *item = [popup itemWithTitle:label];
1603 # endif // USE_IPHONE
1604
1605     if (arg_set) {
1606       NSString *this_val = NULL;
1607       NSString *this_key = [self switchToResource: arg_set
1608                                  opts: opts
1609                                  valRet: &this_val];
1610       NSAssert1 (this_val, @"this_val null for %@", arg_set);
1611       if (menu_key && ![menu_key isEqualToString:this_key])
1612         NSAssert3 (0,
1613                    @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1614                    menu_key, this_key, this_val);
1615       if (this_key)
1616         menu_key = this_key;
1617
1618       /* If this menu has the cmd line "-mode foo" then set this item's
1619          value to "foo" (the menu itself will be bound to e.g. "modeString")
1620        */
1621 # ifndef USE_IPHONE
1622       set_menu_item_object (item, this_val);
1623 # else
1624       // Array holds ["Label", "resource-key", "resource-val"].
1625       [items addObject:[NSMutableArray arrayWithObjects:
1626                                          label, @"", this_val, nil]];
1627 # endif
1628
1629     } else {
1630       // no arg-set -- only one menu item can be missing that.
1631       NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1632 # ifndef USE_IPHONE
1633       def_item = item;
1634 # else
1635       def_item = label;
1636       // Array holds ["Label", "resource-key", "resource-val"].
1637       [items addObject:[NSMutableArray arrayWithObjects:
1638                                          label, @"", @"", nil]];
1639 # endif
1640     }
1641
1642     /* make sure the menu button has room for the text of this item,
1643        and remember the greatest width it has reached.
1644      */
1645 # ifndef USE_IPHONE
1646     [popup setTitle:label];
1647     [popup sizeToFit];
1648     NSRect r = [popup frame];
1649     if (r.size.width > max_width) max_width = r.size.width;
1650 # endif // USE_IPHONE
1651   }
1652   
1653   if (!menu_key) {
1654     NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1655     return;
1656   }
1657
1658   /* We've added all of the menu items.  If there was an item with no
1659      command-line switch, then it's the item that represents the default
1660      value.  Now we must bind to that item as well...  (We have to bind
1661      this one late, because if it was the first item, then we didn't
1662      yet know what resource was associated with this menu.)
1663    */
1664   if (def_item) {
1665     NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1666     NSAssert2 (def_obj, 
1667                @"no default value for resource \"%@\" in menu item \"%@\"",
1668                menu_key,
1669 # ifndef USE_IPHONE
1670                [def_item title]
1671 # else
1672                def_item
1673 # endif
1674                );
1675
1676 # ifndef USE_IPHONE
1677     set_menu_item_object (def_item, def_obj);
1678 # else  // !USE_IPHONE
1679     for (NSMutableArray *a in items) {
1680       // Make sure each array contains the resource key.
1681       [a replaceObjectAtIndex:1 withObject:menu_key];
1682       // Make sure the default item contains the default resource value.
1683       if (def_obj && def_item &&
1684           [def_item isEqualToString:[a objectAtIndex:0]])
1685         [a replaceObjectAtIndex:2 withObject:def_obj];
1686     }
1687 # endif // !USE_IPHONE
1688   }
1689
1690 # ifndef USE_IPHONE
1691 #  ifdef USE_PICKER_VIEW
1692   /* Finish tweaking the menu button itself.
1693    */
1694   if (def_item)
1695     [popup setTitle:[def_item title]];
1696   NSRect r = [popup frame];
1697   r.size.width = max_width;
1698   [popup setFrame:r];
1699 #  endif // USE_PICKER_VIEW
1700 # endif
1701
1702 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1703   [self placeChild:popup on:parent];
1704   [self bindResource:popup key:menu_key];
1705   [popup release];
1706 # endif
1707
1708 # ifdef USE_IPHONE
1709 #  ifdef USE_PICKER_VIEW
1710   // Store the items for this picker in the picker_values array.
1711   // This is so fucking stupid.
1712
1713   unsigned long menu_number = [pref_keys count] - 1;
1714   if (! picker_values)
1715     picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1716   while ([picker_values count] <= menu_number)
1717     [picker_values addObject:[NSArray arrayWithObjects: nil]];
1718   [picker_values replaceObjectAtIndex:menu_number withObject:items];
1719   [popup reloadAllComponents];
1720
1721 #  else  // !USE_PICKER_VIEW
1722
1723   [self placeSeparator];
1724
1725   i = 0;
1726   for (__attribute__((unused)) NSArray *item in items) {
1727     RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1728                                           items:items];
1729     [b setLineBreakMode:NSLineBreakByTruncatingHead];
1730     [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1731     [self placeChild:b on:parent];
1732     i++;
1733   }
1734
1735   [self placeSeparator];
1736
1737 #  endif // !USE_PICKER_VIEW
1738 # endif // !USE_IPHONE
1739
1740 }
1741
1742
1743 /* Creates an uneditable, wrapping NSTextField to display the given
1744    text enclosed by <description> ... </description> in the XML.
1745  */
1746 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1747 {
1748   NSString *text = nil;
1749   NSArray *children = [node children];
1750   NSUInteger i, count = [children count];
1751
1752   for (i = 0; i < count; i++) {
1753     NSXMLNode *child = [children objectAtIndex:i];
1754     NSString *s = [child objectValue];
1755     if (text)
1756       text = [text stringByAppendingString:s];
1757     else
1758       text = s;
1759   }
1760   
1761   text = unwrap (text);
1762   
1763   NSRect rect = [parent frame];
1764   rect.origin.x = rect.origin.y = 0;
1765   rect.size.width = 200;
1766   rect.size.height = 50;  // sized later
1767 # ifndef USE_IPHONE
1768   NSText *lab = [[NSText alloc] initWithFrame:rect];
1769   [lab setEditable:NO];
1770   [lab setDrawsBackground:NO];
1771   [lab setHorizontallyResizable:YES];
1772   [lab setVerticallyResizable:YES];
1773   [lab setString:text];
1774   hreffify (lab);
1775   boldify (lab);
1776   [lab sizeToFit];
1777
1778 # else  // USE_IPHONE
1779
1780 #  ifndef USE_HTML_LABELS
1781
1782   UILabel *lab = [self makeLabel:text];
1783   [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1784   hreffify (lab);
1785
1786 #  else  // USE_HTML_LABELS
1787   HTMLLabel *lab = [[HTMLLabel alloc] 
1788                      initWithText:text
1789                      font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1790   [lab setFrame:rect];
1791   [lab sizeToFit];
1792 #  endif // USE_HTML_LABELS
1793
1794   [self placeSeparator];
1795
1796 # endif // USE_IPHONE
1797
1798   [self placeChild:lab on:parent];
1799   [lab release];
1800 }
1801
1802
1803 /* Creates the NSTextField described by the given XML node.
1804  */
1805 - (void) makeTextField: (NSXMLNode *)node
1806                     on: (NSView *)parent
1807              withLabel: (BOOL) label_p
1808             horizontal: (BOOL) horiz_p
1809 {
1810   NSMutableDictionary *dict = [@{ @"id":     @"",
1811                                   @"_label": @"",
1812                                   @"arg":    @"" }
1813                                 mutableCopy];
1814   [self parseAttrs:dict node:node];
1815   NSString *label = [dict objectForKey:@"_label"];
1816   NSString *arg   = [dict objectForKey:@"arg"];
1817
1818   if (!label && label_p) {
1819     NSAssert1 (0, @"no _label in %@", [node name]);
1820     return;
1821   }
1822
1823   NSAssert1 (arg, @"no arg in %@", label);
1824
1825   NSRect rect;
1826   rect.origin.x = rect.origin.y = 0;    
1827   rect.size.width = rect.size.height = 10;
1828   
1829   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1830
1831 # ifndef USE_IPHONE
1832
1833   // make the default size be around 30 columns; a typical value for
1834   // these text fields is "xscreensaver-text --cols 40".
1835   //
1836   [txt setStringValue:@"123456789 123456789 123456789 "];
1837   [txt sizeToFit];
1838   [[txt cell] setWraps:NO];
1839   [[txt cell] setScrollable:YES];
1840   [txt setStringValue:@""];
1841   
1842 # else  // USE_IPHONE
1843
1844   txt.adjustsFontSizeToFitWidth = YES;
1845   txt.textColor = [UIColor blackColor];
1846   txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1847   txt.placeholder = @"";
1848   txt.borderStyle = UITextBorderStyleRoundedRect;
1849   txt.textAlignment = NSTextAlignmentRight;
1850   txt.keyboardType = UIKeyboardTypeDefault;  // Full kbd
1851   txt.autocorrectionType = UITextAutocorrectionTypeNo;
1852   txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1853   txt.clearButtonMode = UITextFieldViewModeAlways;
1854   txt.returnKeyType = UIReturnKeyDone;
1855   txt.delegate = self;
1856   txt.text = @"";
1857   [txt setEnabled: YES];
1858
1859   rect.size.height = [txt.font lineHeight] * 1.2;
1860   [txt setFrame:rect];
1861
1862 # endif // USE_IPHONE
1863
1864   if (label) {
1865     LABEL *lab = [self makeLabel:label];
1866     [self placeChild:lab on:parent];
1867     [lab release];
1868   }
1869
1870   [self placeChild:txt on:parent right:(label ? YES : NO)];
1871
1872   [self bindSwitch:txt cmdline:arg];
1873   [txt release];
1874 }
1875
1876
1877 /* Creates the NSTextField described by the given XML node,
1878    and hooks it up to a Choose button and a file selector widget.
1879  */
1880 - (void) makeFileSelector: (NSXMLNode *)node
1881                        on: (NSView *)parent
1882                  dirsOnly: (BOOL) dirsOnly
1883                 withLabel: (BOOL) label_p
1884                  editable: (BOOL) editable_p
1885 {
1886 # ifndef USE_IPHONE     // No files. No selectors.
1887   NSMutableDictionary *dict = [@{ @"id":     @"",
1888                                   @"_label": @"",
1889                                   @"arg":    @"" }
1890                                 mutableCopy];
1891   [self parseAttrs:dict node:node];
1892   NSString *label = [dict objectForKey:@"_label"];
1893   NSString *arg   = [dict objectForKey:@"arg"];
1894
1895   if (!label && label_p) {
1896     NSAssert1 (0, @"no _label in %@", [node name]);
1897     return;
1898   }
1899
1900   NSAssert1 (arg, @"no arg in %@", label);
1901
1902   NSRect rect;
1903   rect.origin.x = rect.origin.y = 0;    
1904   rect.size.width = rect.size.height = 10;
1905   
1906   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1907
1908   // make the default size be around 20 columns.
1909   //
1910   [txt setStringValue:@"123456789 123456789 "];
1911   [txt sizeToFit];
1912   [txt setSelectable:YES];
1913   [txt setEditable:editable_p];
1914   [txt setBezeled:editable_p];
1915   [txt setDrawsBackground:editable_p];
1916   [[txt cell] setWraps:NO];
1917   [[txt cell] setScrollable:YES];
1918   [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1919   [txt setStringValue:@""];
1920
1921   LABEL *lab = 0;
1922   if (label) {
1923     lab = [self makeLabel:label];
1924     [self placeChild:lab on:parent];
1925     [lab release];
1926   }
1927
1928   [self placeChild:txt on:parent right:(label ? YES : NO)];
1929
1930   [self bindSwitch:txt cmdline:arg];
1931   [txt release];
1932
1933   // Make the text field and label be the same height, whichever is taller.
1934   if (lab) {
1935     rect = [txt frame];
1936     rect.size.height = ([lab frame].size.height > [txt frame].size.height
1937                         ? [lab frame].size.height
1938                         : [txt frame].size.height);
1939     [txt setFrame:rect];
1940   }
1941
1942   // Now put a "Choose" button next to it.
1943   //
1944   rect.origin.x = rect.origin.y = 0;    
1945   rect.size.width = rect.size.height = 10;
1946   NSButton *choose = [[NSButton alloc] initWithFrame:rect];
1947   [choose setTitle:@"Choose..."];
1948   [choose setBezelStyle:NSRoundedBezelStyle];
1949   [choose sizeToFit];
1950
1951   [self placeChild:choose on:parent right:YES];
1952
1953   // center the Choose button around the midpoint of the text field.
1954   rect = [choose frame];
1955   rect.origin.y = ([txt frame].origin.y + 
1956                    (([txt frame].size.height - rect.size.height) / 2));
1957   [choose setFrameOrigin:rect.origin];
1958
1959   [choose setTarget:[parent window]];
1960   if (dirsOnly)
1961     [choose setAction:@selector(fileSelectorChooseDirsAction:)];
1962   else
1963     [choose setAction:@selector(fileSelectorChooseAction:)];
1964
1965   [choose release];
1966 # endif // !USE_IPHONE
1967 }
1968
1969
1970 # ifndef USE_IPHONE
1971
1972 /* Runs a modal file selector and sets the text field's value to the
1973    selected file or directory.
1974  */
1975 static void
1976 do_file_selector (NSTextField *txt, BOOL dirs_p)
1977 {
1978   NSOpenPanel *panel = [NSOpenPanel openPanel];
1979   [panel setAllowsMultipleSelection:NO];
1980   [panel setCanChooseFiles:!dirs_p];
1981   [panel setCanChooseDirectories:dirs_p];
1982
1983   NSString *file = [txt stringValue];
1984   if ([file length] <= 0) {
1985     file = NSHomeDirectory();
1986     if (dirs_p)
1987       file = [file stringByAppendingPathComponent:@"Pictures"];
1988   }
1989
1990 //  NSString *dir = [file stringByDeletingLastPathComponent];
1991
1992   int result = [panel runModalForDirectory:file //dir
1993                                       file:nil //[file lastPathComponent]
1994                                      types:nil];
1995   if (result == NSOKButton) {
1996     NSArray *files = [panel filenames];
1997     file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
1998     file = [file stringByAbbreviatingWithTildeInPath];
1999     [txt setStringValue:file];
2000
2001     // Fuck me!  Just setting the value of the NSTextField does not cause
2002     // that to end up in the preferences!
2003     //
2004     NSDictionary *dict = [txt infoForBinding:@"value"];
2005     NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
2006     NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
2007     if ([path hasPrefix:@"values."])  // WTF.
2008       path = [path substringFromIndex:7];
2009     [[prefs values] setValue:file forKey:path];
2010
2011 #if 0
2012     // make sure the end of the string is visible.
2013     NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
2014     NSRange range;
2015     range.location = [file length]-3;
2016     range.length = 1;
2017     if (! [[txt window] makeFirstResponder:[txt window]])
2018       [[txt window] endEditingFor:nil];
2019 //    [[txt window] makeFirstResponder:nil];
2020     [fe setSelectedRange:range];
2021 //    [tv scrollRangeToVisible:range];
2022 //    [txt setNeedsDisplay:YES];
2023 //    [[txt cell] setNeedsDisplay:YES];
2024 //    [txt selectAll:txt];
2025 #endif
2026   }
2027 }
2028
2029
2030 /* Returns the NSTextField that is to the left of or above the NSButton.
2031  */
2032 static NSTextField *
2033 find_text_field_of_button (NSButton *button)
2034 {
2035   NSView *parent = [button superview];
2036   NSArray *kids = [parent subviews];
2037   int nkids = [kids count];
2038   int i;
2039   NSTextField *f = 0;
2040   for (i = 0; i < nkids; i++) {
2041     NSObject *kid = [kids objectAtIndex:i];
2042     if ([kid isKindOfClass:[NSTextField class]]) {
2043       f = (NSTextField *) kid;
2044     } else if (kid == button) {
2045       if (! f) abort();
2046       return f;
2047     }
2048   }
2049   abort();
2050 }
2051
2052
2053 - (void) fileSelectorChooseAction:(NSObject *)arg
2054 {
2055   NSButton *choose = (NSButton *) arg;
2056   NSTextField *txt = find_text_field_of_button (choose);
2057   do_file_selector (txt, NO);
2058 }
2059
2060 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2061 {
2062   NSButton *choose = (NSButton *) arg;
2063   NSTextField *txt = find_text_field_of_button (choose);
2064   do_file_selector (txt, YES);
2065 }
2066
2067 #endif // !USE_IPHONE
2068
2069
2070 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2071 {
2072 # ifndef USE_IPHONE
2073   /*
2074     Display Text:
2075      (x)  Computer name and time
2076      ( )  Text       [__________________________]
2077      ( )  Text file  [_________________] [Choose]
2078      ( )  URL        [__________________________]
2079      ( )  Shell Cmd  [__________________________]
2080
2081     textMode -text-mode date
2082     textMode -text-mode literal   textLiteral -text-literal %
2083     textMode -text-mode file      textFile    -text-file %
2084     textMode -text-mode url       textURL     -text-url %
2085     textMode -text-mode program   textProgram -text-program %
2086    */
2087   NSRect rect;
2088   rect.size.width = rect.size.height = 1;
2089   rect.origin.x = rect.origin.y = 0;
2090   NSView *group  = [[NSView alloc] initWithFrame:rect];
2091   NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2092
2093   Bool program_p = TRUE;
2094
2095
2096   NSView *control;
2097
2098   // This is how you link radio buttons together.
2099   //
2100   NSButtonCell *proto = [[NSButtonCell alloc] init];
2101   [proto setButtonType:NSRadioButton];
2102
2103   rect.origin.x = rect.origin.y = 0;
2104   rect.size.width = rect.size.height = 10;
2105   NSMatrix *matrix = [[NSMatrix alloc] 
2106                        initWithFrame:rect
2107                        mode:NSRadioModeMatrix
2108                        prototype:proto
2109                        numberOfRows: 4 + (program_p ? 1 : 0)
2110                        numberOfColumns:1];
2111   [matrix setAllowsEmptySelection:NO];
2112
2113   NSArrayController *cnames  = [[NSArrayController alloc] initWithContent:nil];
2114   [cnames addObject:@"Computer name and time"];
2115   [cnames addObject:@"Text"];
2116   [cnames addObject:@"File"];
2117   [cnames addObject:@"URL"];
2118   if (program_p) [cnames addObject:@"Shell Cmd"];
2119   [matrix bind:@"content"
2120           toObject:cnames
2121           withKeyPath:@"arrangedObjects"
2122           options:nil];
2123   [cnames release];
2124
2125   [self bindSwitch:matrix cmdline:@"-text-mode %"];
2126
2127   [self placeChild:matrix on:group];
2128   [self placeChild:rgroup on:group right:YES];
2129
2130   NSXMLNode *node2;
2131
2132 # else  // USE_IPHONE
2133
2134   NSView *rgroup = parent;
2135   NSXMLNode *node2;
2136
2137   // <select id="textMode">
2138   //   <option id="date"  _label="Display date" arg-set="-text-mode date"/>
2139   //   <option id="text"  _label="Display text" arg-set="-text-mode literal"/>
2140   //   <option id="url"   _label="Display URL"/>
2141   // </select>
2142
2143   node2 = [[NSXMLElement alloc] initWithName:@"select"];
2144   [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2145
2146   NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2147   [node3 setAttributesAsDictionary:
2148            @{ @"id":      @"date",
2149               @"arg-set": @"-text-mode date",
2150               @"_label":  @"Display the date and time" }];
2151   [node3 setParent: node2];
2152   //[node3 release];
2153
2154   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2155   [node3 setAttributesAsDictionary:
2156            @{ @"id":      @"text",
2157               @"arg-set": @"-text-mode literal",
2158               @"_label":  @"Display static text" }];
2159   [node3 setParent: node2];
2160   //[node3 release];
2161
2162   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2163   [node3 setAttributesAsDictionary:
2164            @{ @"id":     @"url",                           
2165               @"_label": @"Display the contents of a URL" }];
2166   [node3 setParent: node2];
2167   //[node3 release];
2168
2169   [self makeOptionMenu:node2 on:rgroup];
2170
2171 # endif // USE_IPHONE
2172
2173
2174   //  <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2175   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2176   [node2 setAttributesAsDictionary:
2177            @{ @"id":     @"textLiteral",
2178               @"arg":    @"-text-literal %",
2179 # ifdef USE_IPHONE
2180               @"_label": @"Text to display"
2181 # endif
2182             }];
2183   [self makeTextField:node2 on:rgroup 
2184 # ifndef USE_IPHONE
2185         withLabel:NO
2186 # else
2187         withLabel:YES
2188 # endif
2189         horizontal:NO];
2190
2191 //  rect = [last_child(rgroup) frame];
2192
2193 /* // trying to make the text fields be enabled only when the checkbox is on..
2194   control = last_child (rgroup);
2195   [control bind:@"enabled"
2196            toObject:[matrix cellAtRow:1 column:0]
2197            withKeyPath:@"value"
2198            options:nil];
2199  */
2200
2201
2202 # ifndef USE_IPHONE
2203   //  <file id="textFile" _label="" arg-set="-text-file %"/>
2204   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2205   [node2 setAttributesAsDictionary:
2206            @{ @"id":  @"textFile",
2207               @"arg": @"-text-file %" }];
2208   [self makeFileSelector:node2 on:rgroup
2209         dirsOnly:NO withLabel:NO editable:NO];
2210 # endif // !USE_IPHONE
2211
2212 //  rect = [last_child(rgroup) frame];
2213
2214   //  <string id="textURL" _label="" arg-set="text-url %"/>
2215   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2216   [node2 setAttributesAsDictionary:
2217            @{ @"id":     @"textURL",            
2218               @"arg":    @"-text-url %",
2219 # ifdef USE_IPHONE
2220               @"_label": @"URL to display",     
2221 # endif
2222             }];
2223   [self makeTextField:node2 on:rgroup 
2224 # ifndef USE_IPHONE
2225         withLabel:NO
2226 # else
2227         withLabel:YES
2228 # endif
2229         horizontal:NO];
2230
2231 //  rect = [last_child(rgroup) frame];
2232
2233 # ifndef USE_IPHONE
2234   if (program_p) {
2235     //  <string id="textProgram" _label="" arg-set="text-program %"/>
2236     node2 = [[NSXMLElement alloc] initWithName:@"string"];
2237     [node2 setAttributesAsDictionary:
2238              @{ @"id":   @"textProgram",
2239                  @"arg": @"-text-program %",
2240               }];
2241     [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2242   }
2243
2244 //  rect = [last_child(rgroup) frame];
2245
2246   layout_group (rgroup, NO);
2247
2248   rect = [rgroup frame];
2249   rect.size.width += 35;    // WTF?  Why is rgroup too narrow?
2250   [rgroup setFrame:rect];
2251
2252
2253   // Set the height of the cells in the radio-box matrix to the height of
2254   // the (last of the) text fields.
2255   control = last_child (rgroup);
2256   rect = [control frame];
2257   rect.size.width = 30;  // width of the string "Text", plus a bit...
2258   if (program_p)
2259     rect.size.width += 25;
2260   rect.size.height += LINE_SPACING;
2261   [matrix setCellSize:rect.size];
2262   [matrix sizeToCells];
2263
2264   layout_group (group, YES);
2265   rect = [matrix frame];
2266   rect.origin.x += rect.size.width + COLUMN_SPACING;
2267   rect.origin.y -= [control frame].size.height - LINE_SPACING;
2268   [rgroup setFrameOrigin:rect.origin];
2269
2270   // now cheat on the size of the matrix: allow it to overlap (underlap)
2271   // the text fields.
2272   // 
2273   rect.size = [matrix cellSize];
2274   rect.size.width = 300;
2275   [matrix setCellSize:rect.size];
2276   [matrix sizeToCells];
2277
2278   // Cheat on the position of the stuff on the right (the rgroup).
2279   // GAAAH, this code is such crap!
2280   rect = [rgroup frame];
2281   rect.origin.y -= 5;
2282   [rgroup setFrame:rect];
2283
2284
2285   rect.size.width = rect.size.height = 0;
2286   NSBox *box = [[NSBox alloc] initWithFrame:rect];
2287   [box setTitlePosition:NSAtTop];
2288   [box setBorderType:NSBezelBorder];
2289   [box setTitle:@"Display Text"];
2290
2291   rect.size.width = rect.size.height = 12;
2292   [box setContentViewMargins:rect.size];
2293   [box setContentView:group];
2294   [box sizeToFit];
2295
2296   [self placeChild:box on:parent];
2297
2298 # endif // !USE_IPHONE
2299 }
2300
2301
2302 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2303 {
2304   /*
2305     [x]  Grab desktop images
2306     [ ]  Choose random image:
2307          [__________________________]  [Choose]
2308
2309    <boolean id="grabDesktopImages" _label="Grab desktop images"
2310        arg-unset="-no-grab-desktop"/>
2311    <boolean id="chooseRandomImages" _label="Grab desktop images"
2312        arg-unset="-choose-random-images"/>
2313    <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2314    */
2315
2316   NSXMLElement *node2;
2317
2318 # ifndef USE_IPHONE
2319 #  define SCREENS "Grab desktop images"
2320 #  define PHOTOS  "Choose random images"
2321 # else
2322 #  define SCREENS "Grab screenshots"
2323 #  define PHOTOS  "Use photo library"
2324 # endif
2325
2326   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2327   [node2 setAttributesAsDictionary:
2328            @{ @"id":        @"grabDesktopImages",
2329               @"_label":    @ SCREENS,
2330               @"arg-unset": @"-no-grab-desktop",
2331             }];
2332   [self makeCheckbox:node2 on:parent];
2333
2334   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2335   [node2 setAttributesAsDictionary:
2336            @{ @"id":      @"chooseRandomImages",
2337               @"_label":  @ PHOTOS,
2338               @"arg-set": @"-choose-random-images",
2339             }];
2340   [self makeCheckbox:node2 on:parent];
2341
2342   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2343   [node2 setAttributesAsDictionary:
2344            @{ @"id":     @"imageDirectory",
2345               @"_label": @"Images from:",
2346               @"arg":    @"-image-directory %",
2347             }];
2348   [self makeFileSelector:node2 on:parent
2349         dirsOnly:YES withLabel:YES editable:YES];
2350
2351 # undef SCREENS
2352 # undef PHOTOS
2353
2354 # ifndef USE_IPHONE
2355   // Add a second, explanatory label below the file/URL selector.
2356
2357   LABEL *lab2 = 0;
2358   lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2359   [self placeChild:lab2 on:parent];
2360
2361   // Pack it in a little tighter vertically.
2362   NSRect r2 = [lab2 frame];
2363   r2.origin.x += 20;
2364   r2.origin.y += 14;
2365   [lab2 setFrameOrigin:r2.origin];
2366   [lab2 release];
2367 # endif // USE_IPHONE
2368 }
2369
2370
2371 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2372 {
2373 # ifndef USE_IPHONE
2374   /*
2375     [x]  Check for Updates  [ Monthly ]
2376
2377   <hgroup>
2378    <boolean id="automaticallyChecksForUpdates"
2379             _label="Automatically check for updates"
2380             arg-unset="-no-automaticallyChecksForUpdates" />
2381    <select id="updateCheckInterval">
2382     <option="hourly"  _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2383     <option="daily"   _label="Daily"  arg-set="-updateCheckInterval 86400"/>
2384     <option="weekly"  _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2385     <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2386    </select>
2387   </hgroup>
2388    */
2389
2390   // <hgroup>
2391
2392   NSRect rect;
2393   rect.size.width = rect.size.height = 1;
2394   rect.origin.x = rect.origin.y = 0;
2395   NSView *group = [[NSView alloc] initWithFrame:rect];
2396
2397   NSXMLElement *node2;
2398
2399   // <boolean ...>
2400
2401   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2402   [node2 setAttributesAsDictionary:
2403            @{ @"id":        @SUSUEnableAutomaticChecksKey,
2404               @"_label":    @"Automatically check for updates",
2405               @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2406             }];
2407   [self makeCheckbox:node2 on:group];
2408
2409   // <select ...>
2410
2411   node2 = [[NSXMLElement alloc] initWithName:@"select"];
2412   [node2 setAttributesAsDictionary:
2413            @{ @"id": @SUScheduledCheckIntervalKey }];
2414
2415   //   <option ...>
2416
2417   NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2418   [node3 setAttributesAsDictionary:
2419            @{ @"id":      @"hourly",
2420               @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2421               @"_label":  @"Hourly" }];
2422   [node3 setParent: node2];
2423   //[node3 release];
2424
2425   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2426   [node3 setAttributesAsDictionary:
2427            @{ @"id":      @"daily",
2428               @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2429               @"_label":  @"Daily" }];
2430   [node3 setParent: node2];
2431   //[node3 release];
2432
2433   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2434   [node3 setAttributesAsDictionary:
2435            @{ @"id": @"weekly",
2436            // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2437               @"_label": @"Weekly",
2438             }];
2439   [node3 setParent: node2];
2440   //[node3 release];
2441
2442   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2443   [node3 setAttributesAsDictionary:
2444            @{ @"id":      @"monthly",
2445               @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2446               @"_label":  @"Monthly",
2447              }];
2448   [node3 setParent: node2];
2449   //[node3 release];
2450
2451   // </option>
2452   [self makeOptionMenu:node2 on:group];
2453
2454   // </hgroup>
2455   layout_group (group, TRUE);
2456
2457   rect.size.width = rect.size.height = 0;
2458   NSBox *box = [[NSBox alloc] initWithFrame:rect];
2459   [box setTitlePosition:NSNoTitle];
2460   [box setBorderType:NSNoBorder];
2461   [box setContentViewMargins:rect.size];
2462   [box setContentView:group];
2463   [box sizeToFit];
2464
2465   [self placeChild:box on:parent];
2466
2467   [group release];
2468   [box release];
2469
2470 # endif // !USE_IPHONE
2471 }
2472
2473
2474 #pragma mark Layout for controls
2475
2476
2477 # ifndef USE_IPHONE
2478 static NSView *
2479 last_child (NSView *parent)
2480 {
2481   NSArray *kids = [parent subviews];
2482   int nkids = [kids count];
2483   if (nkids == 0)
2484     return 0;
2485   else
2486     return [kids objectAtIndex:nkids-1];
2487 }
2488 #endif // USE_IPHONE
2489
2490
2491 /* Add the child as a subview of the parent, positioning it immediately
2492    below or to the right of the previously-added child of that view.
2493  */
2494 - (void) placeChild:
2495 # ifdef USE_IPHONE
2496         (NSObject *)child
2497 # else
2498         (NSView *)child
2499 # endif
2500         on:(NSView *)parent right:(BOOL)right_p
2501 {
2502 # ifndef USE_IPHONE
2503   NSRect rect = [child frame];
2504   NSView *last = last_child (parent);
2505   if (!last) {
2506     rect.origin.x = LEFT_MARGIN;
2507     rect.origin.y = ([parent frame].size.height - rect.size.height 
2508                      - LINE_SPACING);
2509   } else if (right_p) {
2510     rect = [last frame];
2511     rect.origin.x += rect.size.width + COLUMN_SPACING;
2512   } else {
2513     rect = [last frame];
2514     rect.origin.x = LEFT_MARGIN;
2515     rect.origin.y -= [child frame].size.height + LINE_SPACING;
2516   }
2517   NSRect r = [child frame];
2518   r.origin = rect.origin;
2519   [child setFrame:r];
2520   [parent addSubview:child];
2521
2522 # else // USE_IPHONE
2523
2524   /* Controls is an array of arrays of the controls, divided into sections.
2525      Each hgroup / vgroup gets a nested array, too, e.g.:
2526
2527        [ [ [ <label>, <checkbox> ],
2528            [ <label>, <checkbox> ],
2529            [ <label>, <checkbox> ] ],
2530          [ <label>, <text-field> ],
2531          [ <label>, <low-label>, <slider>, <high-label> ],
2532          [ <low-label>, <slider>, <high-label> ],
2533          <HTML-label>
2534        ];
2535
2536      If an element begins with a label, it is terminal, otherwise it is a
2537      group.  There are (currently) never more than 4 elements in a single
2538      terminal element.
2539
2540      A blank vertical spacer is placed between each hgroup / vgroup,
2541      by making each of those a new section in the TableView.
2542    */
2543   if (! controls)
2544     controls = [[NSMutableArray arrayWithCapacity:10] retain];
2545   if ([controls count] == 0)
2546     [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2547   NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2548
2549   if (!right_p || [current count] == 0) {
2550     // Nothing on the current line. Add this object.
2551     [current addObject: child];
2552   } else {
2553     // Something's on the current line already.
2554     NSObject *old = [current objectAtIndex:[current count]-1];
2555     if ([old isKindOfClass:[NSMutableArray class]]) {
2556       // Already an array in this cell. Append.
2557       NSAssert ([(NSArray *) old count] < 4, @"internal error");
2558       [(NSMutableArray *) old addObject: child];
2559     } else {
2560       // Replace the control in this cell with an array, then append
2561       NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2562       [current replaceObjectAtIndex:[current count]-1 withObject:a];
2563     }
2564   }
2565 # endif // USE_IPHONE
2566 }
2567
2568
2569 - (void) placeChild:(NSView *)child on:(NSView *)parent
2570 {
2571   [self placeChild:child on:parent right:NO];
2572 }
2573
2574
2575 #ifdef USE_IPHONE
2576
2577 // Start putting subsequent children in a new group, to create a new
2578 // section on the UITableView.
2579 //
2580 - (void) placeSeparator
2581 {
2582   if (! controls) return;
2583   if ([controls count] == 0) return;
2584   if ([[controls objectAtIndex:[controls count]-1]
2585         count] > 0)
2586     [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2587 }
2588 #endif // USE_IPHONE
2589
2590
2591
2592 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2593    wrapped in <hgroup> or <vgroup> in the XML.
2594  */
2595 - (void) makeGroup:(NSXMLNode *)node 
2596                 on:(NSView *)parent
2597         horizontal:(BOOL) horiz_p
2598 {
2599 # ifdef USE_IPHONE
2600   if (!horiz_p) [self placeSeparator];
2601   [self traverseChildren:node on:parent];
2602   if (!horiz_p) [self placeSeparator];
2603 # else  // !USE_IPHONE
2604   NSRect rect;
2605   rect.size.width = rect.size.height = 1;
2606   rect.origin.x = rect.origin.y = 0;
2607   NSView *group = [[NSView alloc] initWithFrame:rect];
2608   [self traverseChildren:node on:group];
2609
2610   layout_group (group, horiz_p);
2611
2612   rect.size.width = rect.size.height = 0;
2613   NSBox *box = [[NSBox alloc] initWithFrame:rect];
2614   [box setTitlePosition:NSNoTitle];
2615   [box setBorderType:NSNoBorder];
2616   [box setContentViewMargins:rect.size];
2617   [box setContentView:group];
2618   [box sizeToFit];
2619
2620   [self placeChild:box on:parent];
2621 # endif // !USE_IPHONE
2622 }
2623
2624
2625 #ifndef USE_IPHONE
2626 static void
2627 layout_group (NSView *group, BOOL horiz_p)
2628 {
2629   NSArray *kids = [group subviews];
2630   int nkids = [kids count];
2631   int i;
2632   double maxx = 0, miny = 0;
2633   for (i = 0; i < nkids; i++) {
2634     NSView *kid = [kids objectAtIndex:i];
2635     NSRect r = [kid frame];
2636     
2637     if (horiz_p) {
2638       maxx += r.size.width + COLUMN_SPACING;
2639       if (r.size.height > -miny) miny = -r.size.height;
2640     } else {
2641       if (r.size.width > maxx)  maxx = r.size.width;
2642       miny = r.origin.y - r.size.height;
2643     }
2644   }
2645   
2646   NSRect rect;
2647   rect.origin.x = 0;
2648   rect.origin.y = 0;
2649   rect.size.width = maxx;
2650   rect.size.height = -miny;
2651   [group setFrame:rect];
2652
2653   double x = 0;
2654   for (i = 0; i < nkids; i++) {
2655     NSView *kid = [kids objectAtIndex:i];
2656     NSRect r = [kid frame];
2657     if (horiz_p) {
2658       r.origin.y = rect.size.height - r.size.height;
2659       r.origin.x = x;
2660       x += r.size.width + COLUMN_SPACING;
2661     } else {
2662       r.origin.y -= miny;
2663     }
2664     [kid setFrame:r];
2665   }
2666 }
2667 #endif // !USE_IPHONE
2668
2669
2670 /* Create some kind of control corresponding to the given XML node.
2671  */
2672 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2673 {
2674   NSString *name = [node name];
2675
2676   if ([node kind] == NSXMLCommentKind)
2677     return;
2678
2679   if ([node kind] == NSXMLTextKind) {
2680     NSString *s = [(NSString *) [node objectValue]
2681                    stringByTrimmingCharactersInSet:
2682                     [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2683     if (! [s isEqualToString:@""]) {
2684       NSAssert1 (0, @"unexpected text: %@", s);
2685     }
2686     return;
2687   }
2688
2689   if ([node kind] != NSXMLElementKind) {
2690     NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2691     return;
2692   }
2693
2694   if ([name isEqualToString:@"hgroup"] ||
2695       [name isEqualToString:@"vgroup"]) {
2696
2697     [self makeGroup:node on:parent 
2698           horizontal:[name isEqualToString:@"hgroup"]];
2699
2700   } else if ([name isEqualToString:@"command"]) {
2701     // do nothing: this is the "-root" business
2702
2703   } else if ([name isEqualToString:@"video"]) {
2704     // ignored
2705
2706   } else if ([name isEqualToString:@"boolean"]) {
2707     [self makeCheckbox:node on:parent];
2708
2709   } else if ([name isEqualToString:@"string"]) {
2710     [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2711
2712   } else if ([name isEqualToString:@"file"]) {
2713     [self makeFileSelector:node on:parent
2714           dirsOnly:NO withLabel:YES editable:NO];
2715
2716   } else if ([name isEqualToString:@"number"]) {
2717     [self makeNumberSelector:node on:parent];
2718
2719   } else if ([name isEqualToString:@"select"]) {
2720     [self makeOptionMenu:node on:parent];
2721
2722   } else if ([name isEqualToString:@"_description"]) {
2723     [self makeDescLabel:node on:parent];
2724
2725   } else if ([name isEqualToString:@"xscreensaver-text"]) {
2726     [self makeTextLoaderControlBox:node on:parent];
2727
2728   } else if ([name isEqualToString:@"xscreensaver-image"]) {
2729     [self makeImageLoaderControlBox:node on:parent];
2730
2731   } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2732     [self makeUpdaterControlBox:node on:parent];
2733
2734   } else {
2735     NSAssert1 (0, @"unknown tag: %@", name);
2736   }
2737 }
2738
2739
2740 /* Iterate over and process the children of this XML node.
2741  */
2742 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2743 {
2744   NSArray *children = [node children];
2745   NSUInteger i, count = [children count];
2746   for (i = 0; i < count; i++) {
2747     NSXMLNode *child = [children objectAtIndex:i];
2748     [self makeControl:child on:parent];
2749   }
2750 }
2751
2752
2753 # ifndef USE_IPHONE
2754
2755 /* Kludgey magic to make the window enclose the controls we created.
2756  */
2757 static void
2758 fix_contentview_size (NSView *parent)
2759 {
2760   NSRect f;
2761   NSArray *kids = [parent subviews];
2762   int nkids = [kids count];
2763   NSView *text = 0;  // the NSText at the bottom of the window
2764   double maxx = 0, miny = 0;
2765   int i;
2766
2767   /* Find the size of the rectangle taken up by each of the children
2768      except the final "NSText" child.
2769   */
2770   for (i = 0; i < nkids; i++) {
2771     NSView *kid = [kids objectAtIndex:i];
2772     if ([kid isKindOfClass:[NSText class]]) {
2773       text = kid;
2774       continue;
2775     }
2776     f = [kid frame];
2777     if (f.origin.x + f.size.width > maxx)  maxx = f.origin.x + f.size.width;
2778     if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2779 //    NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2780 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
2781 //          f.origin.y + f.size.height, [kid class]);
2782   }
2783   
2784   if (maxx < 400) maxx = 400;   // leave room for the NSText paragraph...
2785   
2786   /* Now that we know the width of the window, set the width of the NSText to
2787      that, so that it can decide what its height needs to be.
2788    */
2789   if (! text) abort();
2790   f = [text frame];
2791 //  NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2792 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
2793 //        f.origin.y + f.size.height, [text class]);
2794   
2795   // set the NSText's width (this changes its height).
2796   f.size.width = maxx - LEFT_MARGIN;
2797   [text setFrame:f];
2798   
2799   // position the NSText below the last child (this gives us a new miny).
2800   f = [text frame];
2801   f.origin.y = miny - f.size.height - LINE_SPACING;
2802   miny = f.origin.y - LINE_SPACING;
2803   [text setFrame:f];
2804   
2805   // Lock the width of the field and unlock the height, and let it resize
2806   // once more, to compute the proper height of the text for that width.
2807   //
2808   [(NSText *) text setHorizontallyResizable:NO];
2809   [(NSText *) text setVerticallyResizable:YES];
2810   [(NSText *) text sizeToFit];
2811
2812   // Now lock the height too: no more resizing this text field.
2813   //
2814   [(NSText *) text setVerticallyResizable:NO];
2815
2816   // Now reposition the top edge of the text field to be back where it
2817   // was before we changed the height.
2818   //
2819   float oh = f.size.height;
2820   f = [text frame];
2821   float dh = f.size.height - oh;
2822   f.origin.y += dh;
2823
2824   // #### This is needed in OSX 10.5, but is wrong in OSX 10.6.  WTF??
2825   //      If we do this in 10.6, the text field moves down, off the window.
2826   //      So instead we repair it at the end, at the "WTF2" comment.
2827   [text setFrame:f];
2828
2829   // Also adjust the parent height by the change in height of the text field.
2830   miny -= dh;
2831
2832 //  NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2833 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
2834 //        f.origin.y + f.size.height, [text class]);
2835   
2836   
2837   /* Set the contentView to the size of the children.
2838    */
2839   f = [parent frame];
2840 //  float yoff = f.size.height;
2841   f.size.width = maxx + LEFT_MARGIN;
2842   f.size.height = -(miny - LEFT_MARGIN*2);
2843 //  yoff = f.size.height - yoff;
2844   [parent setFrame:f];
2845
2846 //  NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", 
2847 //        f.size.width, f.size.height, f.origin.x, f.origin.y);
2848
2849   /* Now move all of the kids up into the window.
2850    */
2851   f = [parent frame];
2852   float shift = f.size.height;
2853 //  NSLog(@"shift: %3.0f", shift);
2854   for (i = 0; i < nkids; i++) {
2855     NSView *kid = [kids objectAtIndex:i];
2856     f = [kid frame];
2857     f.origin.y += shift;
2858     [kid setFrame:f];
2859 //    NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2860 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
2861 //          f.origin.y + f.size.height, [kid class]);
2862   }
2863   
2864 /*
2865     Bad:
2866      parent: 420 x 541 @   0   0
2867      text:   380 x 100 @  20  22  miny=-501
2868
2869     Good:
2870      parent: 420 x 541 @   0   0
2871      text:   380 x 100 @  20  50  miny=-501
2872  */
2873
2874   // #### WTF2: See "WTF" above.  If the text field is off the screen,
2875   //      move it up.  We need this on 10.6 but not on 10.5.  Auugh.
2876   //
2877   f = [text frame];
2878   if (f.origin.y < 50) {    // magic numbers, yay
2879     f.origin.y = 50;
2880     [text setFrame:f];
2881   }
2882
2883   /* Set the kids to track the top left corner of the window when resized.
2884      Set the NSText to track the bottom right corner as well.
2885    */
2886   for (i = 0; i < nkids; i++) {
2887     NSView *kid = [kids objectAtIndex:i];
2888     unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2889     if ([kid isKindOfClass:[NSText class]])
2890       mask |= NSViewWidthSizable|NSViewHeightSizable;
2891     [kid setAutoresizingMask:mask];
2892   }
2893 }
2894 # endif // !USE_IPHONE
2895
2896
2897
2898 #ifndef USE_IPHONE
2899 static NSView *
2900 wrap_with_buttons (NSWindow *window, NSView *panel)
2901 {
2902   NSRect rect;
2903   
2904   // Make a box to hold the buttons at the bottom of the window.
2905   //
2906   rect = [panel frame];
2907   rect.origin.x = rect.origin.y = 0;
2908   rect.size.height = 10;
2909   NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2910   [bbox setTitlePosition:NSNoTitle];  
2911   [bbox setBorderType:NSNoBorder];
2912   
2913   // Make some buttons: Default, Cancel, OK
2914   //
2915   rect.origin.x = rect.origin.y = 0;
2916   rect.size.width = rect.size.height = 10;
2917   NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2918   [reset setTitle:@"Reset to Defaults"];
2919   [reset setBezelStyle:NSRoundedBezelStyle];
2920   [reset sizeToFit];
2921
2922   rect = [reset frame];
2923   NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2924   [ok setTitle:@"OK"];
2925   [ok setBezelStyle:NSRoundedBezelStyle];
2926   [ok sizeToFit];
2927   rect = [bbox frame];
2928   rect.origin.x = rect.size.width - [ok frame].size.width;
2929   [ok setFrameOrigin:rect.origin];
2930
2931   rect = [ok frame];
2932   NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2933   [cancel setTitle:@"Cancel"];
2934   [cancel setBezelStyle:NSRoundedBezelStyle];
2935   [cancel sizeToFit];
2936   rect.origin.x -= [cancel frame].size.width + 10;
2937   [cancel setFrameOrigin:rect.origin];
2938
2939   // Bind OK to RET and Cancel to ESC.
2940   [ok     setKeyEquivalent:@"\r"];
2941   [cancel setKeyEquivalent:@"\e"];
2942
2943   // The correct width for OK and Cancel buttons is 68 pixels
2944   // ("Human Interface Guidelines: Controls: Buttons: 
2945   // Push Button Specifications").
2946   //
2947   rect = [ok frame];
2948   rect.size.width = 68;
2949   [ok setFrame:rect];
2950
2951   rect = [cancel frame];
2952   rect.size.width = 68;
2953   [cancel setFrame:rect];
2954
2955   // It puts the buttons in the box or else it gets the hose again
2956   //
2957   [bbox addSubview:ok];
2958   [bbox addSubview:cancel];
2959   [bbox addSubview:reset];
2960   [bbox sizeToFit];
2961   
2962   // make a box to hold the button-box, and the preferences view
2963   //
2964   rect = [bbox frame];
2965   rect.origin.y += rect.size.height;
2966   NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
2967   [pbox setTitlePosition:NSNoTitle];
2968   [pbox setBorderType:NSBezelBorder];
2969
2970   // Enforce a max height on the dialog, so that it's obvious to me
2971   // (on a big screen) when the dialog will fall off the bottom of
2972   // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
2973   {
2974     NSRect f = [panel frame];
2975     int screen_height = (768    // shortest "modern" Mac display
2976                          - 22   // menu bar
2977                          - 56   // System Preferences toolbar
2978                          - 140  // default magnified bottom dock icon
2979                          );
2980     if (f.size.height > screen_height) {
2981       NSLog(@"%@ height was %.0f; clipping to %d", 
2982           [panel class], f.size.height, screen_height);
2983       f.size.height = screen_height;
2984       [panel setFrame:f];
2985     }
2986   }
2987
2988   [pbox addSubview:panel];
2989   [pbox addSubview:bbox];
2990   [pbox sizeToFit];
2991
2992   [reset  setAutoresizingMask:NSViewMaxXMargin];
2993   [cancel setAutoresizingMask:NSViewMinXMargin];
2994   [ok     setAutoresizingMask:NSViewMinXMargin];
2995   [bbox   setAutoresizingMask:NSViewWidthSizable];
2996   
2997   // grab the clicks
2998   //
2999   [ok     setTarget:window];
3000   [cancel setTarget:window];
3001   [reset  setTarget:window];
3002   [ok     setAction:@selector(okAction:)];
3003   [cancel setAction:@selector(cancelAction:)];
3004   [reset  setAction:@selector(resetAction:)];
3005   
3006   [bbox release];
3007
3008   return pbox;
3009 }
3010 #endif // !USE_IPHONE
3011
3012
3013 /* Iterate over and process the children of the root node of the XML document.
3014  */
3015 - (void)traverseTree
3016 {
3017 # ifdef USE_IPHONE
3018   NSView *parent = [self view];
3019 # else
3020   NSWindow *parent = self;
3021 #endif
3022   NSXMLNode *node = xml_root;
3023
3024   if (![[node name] isEqualToString:@"screensaver"]) {
3025     NSAssert (0, @"top level node is not <xscreensaver>");
3026   }
3027
3028   saver_name = [self parseXScreenSaverTag: node];
3029   saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
3030                            withString:@""];
3031   [saver_name retain];
3032   
3033 # ifndef USE_IPHONE
3034
3035   NSRect rect;
3036   rect.origin.x = rect.origin.y = 0;
3037   rect.size.width = rect.size.height = 1;
3038
3039   NSView *panel = [[NSView alloc] initWithFrame:rect];
3040   [self traverseChildren:node on:panel];
3041   fix_contentview_size (panel);
3042
3043   NSView *root = wrap_with_buttons (parent, panel);
3044   [userDefaultsController   setAppliesImmediately:NO];
3045   [globalDefaultsController setAppliesImmediately:NO];
3046
3047   [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
3048
3049   rect = [parent frameRectForContentRect:[root frame]];
3050   [parent setFrame:rect display:NO];
3051   [parent setMinSize:rect.size];
3052   
3053   [parent setContentView:root];
3054         
3055   [panel release];
3056   [root release];
3057
3058 # else  // USE_IPHONE
3059
3060   CGRect r = [parent frame];
3061   r.size = [[UIScreen mainScreen] bounds].size;
3062   [parent setFrame:r];
3063   [self traverseChildren:node on:parent];
3064
3065 # endif // USE_IPHONE
3066 }
3067
3068
3069 - (void)parser:(NSXMLParser *)parser
3070         didStartElement:(NSString *)elt
3071         namespaceURI:(NSString *)ns
3072         qualifiedName:(NSString *)qn
3073         attributes:(NSDictionary *)attrs
3074 {
3075   NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3076   [e setKind:SimpleXMLElementKind];
3077   [e setAttributesAsDictionary:attrs];
3078   NSXMLElement *p = xml_parsing;
3079   [e setParent:p];
3080   xml_parsing = e;
3081   if (! xml_root)
3082     xml_root = xml_parsing;
3083 }
3084
3085 - (void)parser:(NSXMLParser *)parser
3086         didEndElement:(NSString *)elt
3087         namespaceURI:(NSString *)ns
3088         qualifiedName:(NSString *)qn
3089 {
3090   NSXMLElement *p = xml_parsing;
3091   if (! p) {
3092     NSLog(@"extra close: %@", elt);
3093   } else if (![[p name] isEqualToString:elt]) {
3094     NSLog(@"%@ closed by %@", [p name], elt);
3095   } else {
3096     NSXMLElement *n = xml_parsing;
3097     xml_parsing = [n parent];
3098   }
3099 }
3100
3101
3102 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3103 {
3104   NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3105   [e setKind:SimpleXMLTextKind];
3106   NSXMLElement *p = xml_parsing;
3107   [e setParent:p];
3108   [e setObjectValue: string];
3109 }
3110
3111
3112 # ifdef USE_IPHONE
3113 # ifdef USE_PICKER_VIEW
3114
3115 #pragma mark UIPickerView delegate methods
3116
3117 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3118 {
3119   return 1;     // Columns
3120 }
3121
3122 - (NSInteger)pickerView:(UIPickerView *)pv
3123              numberOfRowsInComponent:(NSInteger)column
3124 {
3125   NSAssert (column == 0, @"weird column");
3126   NSArray *a = [picker_values objectAtIndex: [pv tag]];
3127   if (! a) return 0;  // Too early?
3128   return [a count];
3129 }
3130
3131 - (CGFloat)pickerView:(UIPickerView *)pv
3132            rowHeightForComponent:(NSInteger)column
3133 {
3134   return FONT_SIZE;
3135 }
3136
3137 - (CGFloat)pickerView:(UIPickerView *)pv
3138            widthForComponent:(NSInteger)column
3139 {
3140   NSAssert (column == 0, @"weird column");
3141   NSArray *a = [picker_values objectAtIndex: [pv tag]];
3142   if (! a) return 0;  // Too early?
3143
3144   UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3145   CGFloat max = 0;
3146   for (NSArray *a2 in a) {
3147     NSString *s = [a2 objectAtIndex:0];
3148     CGSize r = [s sizeWithFont:f];
3149     if (r.width > max) max = r.width;
3150   }
3151
3152   max *= 1.7;   // WTF!!
3153
3154   if (max > 320)
3155     max = 320;
3156   else if (max < 120)
3157     max = 120;
3158
3159   return max;
3160
3161 }
3162
3163
3164 - (NSString *)pickerView:(UIPickerView *)pv
3165               titleForRow:(NSInteger)row
3166               forComponent:(NSInteger)column
3167 {
3168   NSAssert (column == 0, @"weird column");
3169   NSArray *a = [picker_values objectAtIndex: [pv tag]];
3170   if (! a) return 0;  // Too early?
3171   a = [a objectAtIndex:row];
3172   NSAssert (a, @"internal error");
3173   return [a objectAtIndex:0];
3174 }
3175
3176 # endif // USE_PICKER_VIEW
3177
3178
3179 #pragma mark UITableView delegate methods
3180
3181 - (void) addResetButton
3182 {
3183   [[self navigationItem] 
3184     setRightBarButtonItem: [[UIBarButtonItem alloc]
3185                              initWithTitle: @"Reset to Defaults"
3186                              style: UIBarButtonItemStyleBordered
3187                              target:self
3188                              action:@selector(resetAction:)]];
3189   NSString *s = saver_name;
3190   if ([self view].frame.size.width > 320)
3191     s = [s stringByAppendingString: @" Settings"];
3192   [self navigationItem].title = s;
3193 }
3194
3195
3196 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3197 {
3198   return YES;
3199 }
3200
3201 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3202   // Number of vertically-stacked white boxes.
3203   return [controls count];
3204 }
3205
3206 - (NSInteger)tableView:(UITableView *)tableView
3207              numberOfRowsInSection:(NSInteger)section
3208 {
3209   // Number of lines in each vertically-stacked white box.
3210   NSAssert (controls, @"internal error");
3211   return [[controls objectAtIndex:section] count];
3212 }
3213
3214 - (NSString *)tableView:(UITableView *)tv
3215               titleForHeaderInSection:(NSInteger)section
3216 {
3217   // Titles above each vertically-stacked white box.
3218 //  if (section == 0)
3219 //    return [saver_name stringByAppendingString:@" Settings"];
3220   return nil;
3221 }
3222
3223
3224 - (CGFloat)tableView:(UITableView *)tv
3225            heightForRowAtIndexPath:(NSIndexPath *)ip
3226 {
3227   CGFloat h = 0;
3228
3229   NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3230                   objectAtIndex:[ip indexAtPosition:1]];
3231
3232   if ([ctl isKindOfClass:[NSArray class]]) {
3233     NSArray *set = (NSArray *) ctl;
3234     switch ([set count]) {
3235     case 4:                     // label + left/slider/right.
3236     case 3:                     // left/slider/right.
3237       h = FONT_SIZE * 3.0;
3238       break;
3239     case 2:                     // Checkboxes, or text fields.
3240       h = FONT_SIZE * 2.4;
3241       break;
3242     }
3243   } else if ([ctl isKindOfClass:[UILabel class]]) {
3244     // Radio buttons in a multi-select list.
3245     h = FONT_SIZE * 1.9;
3246
3247 # ifdef USE_HTML_LABELS
3248   } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3249     
3250     HTMLLabel *t = (HTMLLabel *) ctl;
3251     CGRect r = t.frame;
3252     r.size.width = [tv frame].size.width;
3253     r.size.width -= LEFT_MARGIN * 2;
3254     [t setFrame:r];
3255     [t sizeToFit];
3256     r = t.frame;
3257     h = r.size.height;
3258 # endif // USE_HTML_LABELS
3259
3260   } else {                      // Does this ever happen?
3261     h = FONT_SIZE + LINE_SPACING * 2;
3262   }
3263
3264   if (h <= 0) abort();
3265   return h;
3266 }
3267
3268
3269 - (void)refreshTableView
3270 {
3271   UITableView *tv = (UITableView *) [self view];
3272   NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3273   NSInteger rows = [self numberOfSectionsInTableView:tv];
3274   for (int i = 0; i < rows; i++) {
3275     NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3276     for (int j = 0; j < cols; j++) {
3277       NSUInteger ip[2];
3278       ip[0] = i;
3279       ip[1] = j;
3280       [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3281     }
3282   }
3283
3284   [tv beginUpdates];
3285   [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3286   [tv endUpdates];
3287
3288   // Default opacity looks bad.
3289   // #### Oh great, this only works *sometimes*.
3290   UIView *v = [[self navigationItem] titleView];
3291   [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
3292 }
3293
3294
3295 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3296 {
3297   [NSTimer scheduledTimerWithTimeInterval: 0
3298            target:self
3299            selector:@selector(refreshTableView)
3300            userInfo:nil
3301            repeats:NO];
3302 }
3303
3304
3305 #ifndef USE_PICKER_VIEW
3306
3307 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3308                       button:(RadioButton *)b
3309 {
3310   NSArray *item = [[b items] objectAtIndex: [b index]];
3311   NSString *pref_key = [item objectAtIndex:1];
3312   NSObject *pref_val = [item objectAtIndex:2];
3313
3314   NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3315
3316   // Convert them both to strings and compare those, so that
3317   // we don't get screwed by int 1 versus string "1".
3318   // Will boolean true/1 screw us here too?
3319   //
3320   NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3321                         ? (NSString *) pref_val
3322                         : [(NSNumber *) pref_val stringValue]);
3323   NSString *current_str = ([current isKindOfClass:[NSString class]]
3324                            ? (NSString *) current
3325                            : [(NSNumber *) current stringValue]);
3326   BOOL match_p = [current_str isEqualToString:pref_str];
3327
3328   // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3329
3330   if (match_p)
3331     [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3332   else
3333     [cell setAccessoryType:UITableViewCellAccessoryNone];
3334 }
3335
3336
3337 - (void)tableView:(UITableView *)tv
3338         didSelectRowAtIndexPath:(NSIndexPath *)ip
3339 {
3340   RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3341                        objectAtIndex:[ip indexAtPosition:1]];
3342   if (! [ctl isKindOfClass:[RadioButton class]])
3343     return;
3344
3345   [self radioAction:ctl];
3346   [self refreshTableView];
3347 }
3348
3349
3350 #endif // !USE_PICKER_VIEW
3351
3352
3353
3354 - (UITableViewCell *)tableView:(UITableView *)tv
3355                      cellForRowAtIndexPath:(NSIndexPath *)ip
3356 {
3357   CGFloat ww = [tv frame].size.width;
3358   CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
3359
3360   float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
3361
3362   // Width of the column of labels on the left.
3363   CGFloat left_width = ww * 0.4;
3364   CGFloat right_edge = ww - LEFT_MARGIN;
3365
3366   if (os_version < 7)  // margins were wider on iOS 6.1
3367     right_edge -= 10;
3368
3369   CGFloat max = FONT_SIZE * 12;
3370   if (left_width > max) left_width = max;
3371
3372   NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3373                            objectAtIndex:[ip indexAtPosition:1]];
3374
3375   if ([ctl isKindOfClass:[NSArray class]]) {
3376     // This cell has a set of objects in it.
3377     NSArray *set = (NSArray *) ctl;
3378     switch ([set count]) {
3379     case 2:
3380       {
3381         // With 2 elements, the first of the pair must be a label.
3382         UILabel *label = (UILabel *) [set objectAtIndex: 0];
3383         NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3384         ctl = [set objectAtIndex: 1];
3385
3386         CGRect r = [ctl frame];
3387
3388         if ([ctl isKindOfClass:[UISwitch class]]) {     // Checkboxes.
3389           r.size.width = 80;  // Magic.
3390           r.origin.x = right_edge - r.size.width + 30;  // beats me
3391
3392           if (os_version < 7)  // checkboxes were wider on iOS 6.1
3393             r.origin.x -= 25;
3394
3395         } else {
3396           r.origin.x = left_width;                      // Text fields, etc.
3397           r.size.width = right_edge - r.origin.x;
3398         }
3399
3400         r.origin.y = (hh - r.size.height) / 2;   // Center vertically.
3401         [ctl setFrame:r];
3402
3403         // Make a box and put the label and checkbox/slider into it.
3404         r.origin.x = 0;
3405         r.origin.y = 0;
3406         r.size.width  = ww;
3407         r.size.height = hh;
3408         NSView *box = [[UIView alloc] initWithFrame:r];
3409         [box addSubview: ctl];
3410
3411         // Let the label make use of any space not taken up by the control.
3412         r = [label frame];
3413         r.origin.x = LEFT_MARGIN;
3414         r.origin.y = 0;
3415         r.size.width  = [ctl frame].origin.x - r.origin.x;
3416         r.size.height = hh;
3417         [label setFrame:r];
3418         [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3419         [box addSubview: label];
3420
3421         ctl = box;
3422       }
3423       break;
3424     case 3:
3425     case 4:
3426       {
3427         // With 3 elements, 1 and 3 are labels.
3428         // With 4 elements, 1, 2 and 4 are labels.
3429         int i = 0;
3430         UILabel *top  = ([set count] == 4
3431                          ? [set objectAtIndex: i++]
3432                          : 0);
3433         UILabel *left  = [set objectAtIndex: i++];
3434         NSView  *mid   = [set objectAtIndex: i++];
3435         UILabel *right = [set objectAtIndex: i++];
3436         NSAssert (!top || [top   isKindOfClass:[UILabel class]], @"WTF");
3437         NSAssert (        [left  isKindOfClass:[UILabel class]], @"WTF");
3438         NSAssert (       ![mid   isKindOfClass:[UILabel class]], @"WTF");
3439         NSAssert (        [right isKindOfClass:[UILabel class]], @"WTF");
3440
3441         // 3 elements: control at top of cell.
3442         // 4 elements: center the control vertically.
3443         CGRect r = [mid frame];
3444         r.size.height = 32;   // Unchangable height of the slider thumb.
3445
3446         // Center the slider between left_width and right_edge.
3447 # ifdef  LABEL_ABOVE_SLIDER
3448         r.origin.x = LEFT_MARGIN;
3449 # else
3450         r.origin.x = left_width;
3451 # endif
3452         r.origin.y = (hh - r.size.height) / 2;
3453         r.size.width = right_edge - r.origin.x;
3454         [mid setFrame:r];
3455
3456         if (top) {
3457           r.size = [[top text] sizeWithFont:[top font]
3458                                constrainedToSize:
3459                                  CGSizeMake (ww - LEFT_MARGIN*2, 100000)
3460                                lineBreakMode:[top lineBreakMode]];
3461 # ifdef LABEL_ABOVE_SLIDER
3462           // Top label goes above, flush center/top.
3463           r.origin.x = (ww - r.size.width) / 2;
3464           r.origin.y = 4;
3465 # else  // !LABEL_ABOVE_SLIDER
3466           // Label goes on the left.
3467           r.origin.x = LEFT_MARGIN;
3468           r.origin.y = 0;
3469           r.size.width  = left_width - LEFT_MARGIN;
3470           r.size.height = hh;
3471 # endif // !LABEL_ABOVE_SLIDER
3472           [top setFrame:r];
3473         }
3474
3475         // Left label goes under control, flush left/bottom.
3476         r.size = [[left text] sizeWithFont:[left font]
3477                               constrainedToSize:
3478                                 CGSizeMake(ww - LEFT_MARGIN*2, 100000)
3479                               lineBreakMode:[left lineBreakMode]];
3480         r.origin.x = [mid frame].origin.x;
3481         r.origin.y = hh - r.size.height - 4;
3482         [left setFrame:r];
3483
3484         // Right label goes under control, flush right/bottom.
3485         r = [right frame];
3486         r.size = [[right text] sizeWithFont:[right font]
3487                                constrainedToSize:
3488                                  CGSizeMake(ww - LEFT_MARGIN*2, 1000000)
3489                                lineBreakMode:[right lineBreakMode]];
3490         r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3491                       r.size.width);
3492         r.origin.y = [left frame].origin.y;
3493         [right setFrame:r];
3494
3495         // Make a box and put the labels and slider into it.
3496         r.origin.x = 0;
3497         r.origin.y = 0;
3498         r.size.width  = ww;
3499         r.size.height = hh;
3500         NSView *box = [[UIView alloc] initWithFrame:r];
3501         if (top)
3502           [box addSubview: top];
3503         [box addSubview: left];
3504         [box addSubview: right];
3505         [box addSubview: mid];
3506
3507         ctl = box;
3508       }
3509       break;
3510     default:
3511       NSAssert (0, @"unhandled size");
3512     }
3513   } else {      // A single view, not a pair.
3514     CGRect r = [ctl frame];
3515     r.origin.x = LEFT_MARGIN;
3516     r.origin.y = 0;
3517     r.size.width = right_edge - r.origin.x;
3518     r.size.height = hh;
3519     [ctl setFrame:r];
3520   }
3521
3522   NSString *id = @"Cell";
3523   UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
3524   if (!cell)
3525     cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3526                                      reuseIdentifier: id]
3527              autorelease];
3528
3529   for (UIView *subview in [cell.contentView subviews])
3530     [subview removeFromSuperview];
3531   [cell.contentView addSubview: ctl];
3532   CGRect r = [ctl frame];
3533   r.origin.x = 0;
3534   r.origin.y = 0;
3535   [cell setFrame:r];
3536   cell.selectionStyle = UITableViewCellSelectionStyleNone;
3537   [cell setAccessoryType:UITableViewCellAccessoryNone];
3538
3539 # ifndef USE_PICKER_VIEW
3540   if ([ctl isKindOfClass:[RadioButton class]])
3541     [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3542 # endif // USE_PICKER_VIEW
3543
3544   return cell;
3545 }
3546 # endif  // USE_IPHONE
3547
3548
3549 /* When this object is instantiated, it parses the XML file and creates
3550    controls on itself that are hooked up to the appropriate preferences.
3551    The default size of the view is just big enough to hold them all.
3552  */
3553 - (id)initWithXML: (NSData *) xml_data
3554           options: (const XrmOptionDescRec *) _opts
3555        controller: (NSUserDefaultsController *) _prefs
3556  globalController: (NSUserDefaultsController *) _globalPrefs
3557          defaults: (NSDictionary *) _defs
3558 {
3559 # ifndef USE_IPHONE
3560   self = [super init];
3561 # else  // !USE_IPHONE
3562   self = [super initWithStyle:UITableViewStyleGrouped];
3563   self.title = [saver_name stringByAppendingString:@" Settings"];
3564 # endif // !USE_IPHONE
3565   if (! self) return 0;
3566
3567   // instance variables
3568   opts = _opts;
3569   defaultOptions = _defs;
3570   userDefaultsController   = [_prefs retain];
3571   globalDefaultsController = [_globalPrefs retain];
3572
3573   NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3574
3575   if (!xmlDoc) {
3576     NSAssert1 (0, @"XML Error: %@",
3577                [[NSString alloc] initWithData:xml_data
3578                                  encoding:NSUTF8StringEncoding]);
3579     return nil;
3580   }
3581   [xmlDoc setDelegate:self];
3582   if (! [xmlDoc parse]) {
3583     NSError *err = [xmlDoc parserError];
3584     NSAssert2 (0, @"XML Error: %@: %@",
3585                [[NSString alloc] initWithData:xml_data
3586                                  encoding:NSUTF8StringEncoding],
3587                err);
3588     return nil;
3589   }
3590
3591   [self traverseTree];
3592   xml_root = 0;
3593
3594 # ifdef USE_IPHONE
3595   [self addResetButton];
3596 # endif
3597
3598   return self;
3599 }
3600
3601
3602 - (void) dealloc
3603 {
3604   [saver_name release];
3605   [userDefaultsController release];
3606   [globalDefaultsController release];
3607 # ifdef USE_IPHONE
3608   [controls release];
3609   [pref_keys release];
3610   [pref_ctls release];
3611 #  ifdef USE_PICKER_VIEW
3612   [picker_values release];
3613 #  endif
3614 # endif
3615   [super dealloc];
3616 }
3617
3618 @end