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