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