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