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