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