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