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