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