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