http://www.tienza.es/crux/src/www.jwz.org/xscreensaver/xscreensaver-5.05.tar.gz
[xscreensaver] / OSX / XScreenSaverConfigSheet.m
1 /* xscreensaver, Copyright (c) 2006 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 #import <Foundation/NSXMLDocument.h>
31
32 @implementation XScreenSaverConfigSheet
33
34 #define LEFT_MARGIN       20   // left edge of window
35 #define COLUMN_SPACING    10   // gap between e.g. labels and text fields
36 #define LEFT_LABEL_WIDTH  70   // width of all left labels
37 #define LINE_SPACING      10   // leading between each line
38
39 // redefine these since they don't work when not inside an ObjC method
40 #undef NSAssert
41 #undef NSAssert1
42 #undef NSAssert2
43 #undef NSAssert3
44 #define NSAssert(CC,S)        do { if (!(CC)) { NSLog(S);       }} while(0)
45 #define NSAssert1(CC,S,A)     do { if (!(CC)) { NSLog(S,A);     }} while(0)
46 #define NSAssert2(CC,S,A,B)   do { if (!(CC)) { NSLog(S,A,B);   }} while(0)
47 #define NSAssert3(CC,S,A,B,C) do { if (!(CC)) { NSLog(S,A,B,C); }} while(0)
48
49
50 /* Given a command-line option, returns the corresponding resource name.
51    Any arguments in the switch string are ignored (e.g., "-foo x").
52  */
53 static NSString *
54 switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts,
55                     NSString **val_ret)
56 {
57   char buf[255];
58   char *tail = 0;
59   NSAssert(cmdline_switch, @"cmdline switch is null");
60   if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
61                           encoding:NSUTF8StringEncoding]) {
62     NSAssert1(0, @"unable to convert %@", cmdline_switch);
63     abort();
64   }
65   char *s = strpbrk(buf, " \t\r\n");
66   if (s && *s) {
67     *s = 0;
68     tail = s+1;
69     while (*tail && (*tail == ' ' || *tail == '\t'))
70       tail++;
71   }
72   
73   while (opts[0].option) {
74     if (!strcmp (opts[0].option, buf)) {
75       const char *ret = 0;
76
77       if (opts[0].argKind == XrmoptionNoArg) {
78         if (tail && *tail)
79           NSAssert1 (0, @"expected no args to switch: \"%@\"",
80                      cmdline_switch);
81         ret = opts[0].value;
82       } else {
83         if (!tail || !*tail)
84           NSAssert1 (0, @"expected args to switch: \"%@\"",
85                      cmdline_switch);
86         ret = tail;
87       }
88
89       if (val_ret)
90         *val_ret = (ret
91                     ? [NSString stringWithCString:ret
92                                          encoding:NSUTF8StringEncoding]
93                     : 0);
94       
95       const char *res = opts[0].specifier;
96       while (*res && (*res == '.' || *res == '*'))
97         res++;
98       return [NSString stringWithCString:res
99                                 encoding:NSUTF8StringEncoding];
100     }
101     opts++;
102   }
103   
104   NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
105   abort();
106 }
107
108
109 /* Connects a control (checkbox, etc) to the corresponding preferences key.
110  */
111 static void
112 bind_resource_to_preferences (NSUserDefaultsController *prefs,
113                               NSObject *control, 
114                               NSString *pref_key,
115                               const XrmOptionDescRec *opts)
116 {
117   NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
118                       ? @"selectedObject"
119                       : ([control isKindOfClass:[NSMatrix class]]
120                          ? @"selectedIndex"
121                          : @"value"));
122   [control bind:bindto
123        toObject:prefs
124     withKeyPath:[@"values." stringByAppendingString: pref_key]
125         options:nil];
126
127 # if 0 // ####
128   NSObject *def = [[prefs defaults] objectForKey:pref_key];
129   NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
130   s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
131   s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
132   s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
133   NSLog (@"%@ %@/%@", s, [def class], [control class]);
134 # endif
135 }
136
137 static void
138 bind_switch_to_preferences (NSUserDefaultsController *prefs,
139                             NSObject *control, 
140                             NSString *cmdline_switch,
141                             const XrmOptionDescRec *opts)
142 {
143   NSString *pref_key = switch_to_resource (cmdline_switch, opts, 0);
144   bind_resource_to_preferences (prefs, control, pref_key, opts);
145 }
146
147
148 /* Parse the attributes of an XML tag into a dictionary.
149    For input, the dictionary should have as attributes the keys, each
150    with @"" as their value.
151    On output, the dictionary will set the keys to the values specified,
152    and keys that were not specified will not be present in the dictionary.
153    Warnings are printed if there are duplicate or unknown attributes.
154  */
155 static void
156 parse_attrs (NSMutableDictionary *dict, NSXMLNode *node)
157 {
158   NSArray *attrs = [(NSXMLElement *) node attributes];
159   int n = [attrs count];
160   int i;
161   
162   // For each key in the dictionary, fill in the dict with the corresponding
163   // value.  The value @"" is assumed to mean "un-set".  Issue a warning if
164   // an attribute is specified twice.
165   //
166   for (i = 0; i < n; i++) {
167     NSXMLNode *attr = [attrs objectAtIndex:i];
168     NSString *key = [attr name];
169     NSString *val = [attr objectValue];
170     NSString *old = [dict objectForKey:key];
171     
172     if (! old) {
173       NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
174     } else if ([old length] != 0) {
175       NSAssert2 (0, @"duplicate %@: \"%@\", \"%@\"", old, val);
176     } else {
177       [dict setValue:val forKey:key];
178     }
179   }
180   
181   // Remove from the dictionary any keys whose value is still @"", 
182   // meaning there was no such attribute specified.
183   //
184   NSArray *keys = [dict allKeys];
185   n = [keys count];
186   for (i = 0; i < n; i++) {
187     NSString *key = [keys objectAtIndex:i];
188     NSString *val = [dict objectForKey:key];
189     if ([val length] == 0)
190       [dict removeObjectForKey:key];
191   }
192 }
193
194
195 /* Creates a label: an un-editable NSTextField displaying the given text.
196  */
197 static NSTextField *
198 make_label (NSString *text)
199 {
200   NSRect rect;
201   rect.origin.x = rect.origin.y = 0;
202   rect.size.width = rect.size.height = 10;
203   NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
204   [lab setSelectable:NO];
205   [lab setEditable:NO];
206   [lab setBezeled:NO];
207   [lab setDrawsBackground:NO];
208   [lab setStringValue:text];
209   [lab sizeToFit];
210   return lab;
211 }
212
213
214 static NSView *
215 last_child (NSView *parent)
216 {
217   NSArray *kids = [parent subviews];
218   int nkids = [kids count];
219   if (nkids == 0)
220     return 0;
221   else
222     return [kids objectAtIndex:nkids-1];
223 }
224
225
226 /* Add the child as a subview of the parent, positioning it immediately
227    below or to the right of the previously-added child of that view.
228  */
229 static void
230 place_child (NSView *parent, NSView *child, BOOL right_p)
231 {
232   NSRect rect = [child frame];
233   NSView *last = last_child (parent);
234   if (!last) {
235     rect.origin.x = LEFT_MARGIN;
236     rect.origin.y = [parent frame].size.height - rect.size.height 
237       - LINE_SPACING;
238   } else if (right_p) {
239     rect = [last frame];
240     rect.origin.x += rect.size.width + COLUMN_SPACING;
241   } else {
242     rect = [last frame];
243     rect.origin.x = LEFT_MARGIN;
244     rect.origin.y -= [child frame].size.height + LINE_SPACING;
245   }
246   [child setFrameOrigin:rect.origin];
247   [parent addSubview:child];
248 }
249
250
251 static void traverse_children (NSUserDefaultsController *,
252                                const XrmOptionDescRec *, 
253                                NSView *, NSXMLNode *);
254
255
256 /* Creates the checkbox (NSButton) described by the given XML node.
257  */
258 static void
259 make_checkbox (NSUserDefaultsController *prefs,
260                const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
261 {
262   NSMutableDictionary *dict =
263     [NSMutableDictionary dictionaryWithObjectsAndKeys:
264       @"", @"id",
265       @"", @"_label",
266       @"", @"arg-set",
267       @"", @"arg-unset",
268       nil];
269   parse_attrs (dict, node);
270   NSString *label     = [dict objectForKey:@"_label"];
271   NSString *arg_set   = [dict objectForKey:@"arg-set"];
272   NSString *arg_unset = [dict objectForKey:@"arg-unset"];
273   
274   if (!label) {
275     NSAssert1 (0, @"no _label in %@", [node name]);
276     return;
277   }
278   if (!arg_set && !arg_unset) {
279     NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"", 
280                label);
281   }
282   if (arg_set && arg_unset) {
283     NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"", 
284                label);
285   }
286   
287   // sanity-check the choice of argument names.
288   //
289   if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
290                   [arg_set hasPrefix:@"--no-"]))
291     NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
292            label, arg_set);
293   if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
294                     ![arg_unset hasPrefix:@"--no-"]))
295     NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
296           label, arg_unset);
297     
298   NSRect rect;
299   rect.origin.x = rect.origin.y = 0;
300   rect.size.width = rect.size.height = 10;
301
302   NSButton *button = [[NSButton alloc] initWithFrame:rect];
303   [button setButtonType:([[node name] isEqualToString:@"radio"]
304                          ? NSRadioButton
305                          : NSSwitchButton)];
306   [button setTitle:label];
307   [button sizeToFit];
308   place_child (parent, button, NO);
309   
310   bind_switch_to_preferences (prefs, button,
311                               (arg_set ? arg_set : arg_unset),
312                               opts);
313   [button release];
314 }
315
316
317 /* Creates the NSTextField described by the given XML node.
318 */
319 static void
320 make_text_field (NSUserDefaultsController *prefs,
321                  const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node,
322                  BOOL no_label_p)
323 {
324   NSMutableDictionary *dict =
325   [NSMutableDictionary dictionaryWithObjectsAndKeys:
326     @"", @"id",
327     @"", @"_label",
328     @"", @"arg",
329     nil];
330   parse_attrs (dict, node);
331   NSString *label = [dict objectForKey:@"_label"];
332   NSString *arg   = [dict objectForKey:@"arg"];
333
334   if (!label && !no_label_p) {
335     NSAssert1 (0, @"no _label in %@", [node name]);
336     return;
337   }
338
339   NSAssert1 (arg, @"no arg in %@", label);
340
341   NSRect rect;
342   rect.origin.x = rect.origin.y = 0;    
343   rect.size.width = rect.size.height = 10;
344   
345   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
346
347   // make the default size be around 30 columns; a typical value for
348   // these text fields is "xscreensaver-text --cols 40".
349   //
350   [txt setStringValue:@"123456789 123456789 123456789 "];
351   [txt sizeToFit];
352   [[txt cell] setWraps:NO];
353   [[txt cell] setScrollable:YES];
354   [txt setStringValue:@""];
355   
356   if (label) {
357     NSTextField *lab = make_label (label);
358     place_child (parent, lab, NO);
359     [lab release];
360   }
361
362   place_child (parent, txt, (label ? YES : NO));
363
364   bind_switch_to_preferences (prefs, txt, arg, opts);
365   [txt release];
366 }
367
368
369 /* Creates the NSTextField described by the given XML node,
370    and hooks it up to a Choose button and a file selector widget.
371 */
372 static void
373 make_file_selector (NSUserDefaultsController *prefs,
374                     const XrmOptionDescRec *opts, 
375                     NSView *parent, NSXMLNode *node,
376                     BOOL dirs_only_p,
377                     BOOL no_label_p)
378 {
379   NSMutableDictionary *dict =
380   [NSMutableDictionary dictionaryWithObjectsAndKeys:
381     @"", @"id",
382     @"", @"_label",
383     @"", @"arg",
384     nil];
385   parse_attrs (dict, node);
386   NSString *label = [dict objectForKey:@"_label"];
387   NSString *arg   = [dict objectForKey:@"arg"];
388
389   if (!label && !no_label_p) {
390     NSAssert1 (0, @"no _label in %@", [node name]);
391     return;
392   }
393
394   NSAssert1 (arg, @"no arg in %@", label);
395
396   NSRect rect;
397   rect.origin.x = rect.origin.y = 0;    
398   rect.size.width = rect.size.height = 10;
399   
400   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
401
402   // make the default size be around 20 columns.
403   //
404   [txt setStringValue:@"123456789 123456789 "];
405   [txt sizeToFit];
406   [txt setSelectable:YES];
407   [txt setEditable:NO];
408   [txt setBezeled:NO];
409   [txt setDrawsBackground:NO];
410   [[txt cell] setWraps:NO];
411   [[txt cell] setScrollable:YES];
412   [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
413   [txt setStringValue:@""];
414
415   NSTextField *lab = 0;
416   if (label) {
417     lab = make_label (label);
418     place_child (parent, lab, NO);
419     [lab release];
420   }
421
422   place_child (parent, txt, (label ? YES : NO));
423
424   bind_switch_to_preferences (prefs, txt, arg, opts);
425   [txt release];
426
427   // Make the text field be the same height as the label.
428   if (lab) {
429     rect = [txt frame];
430     rect.size.height = [lab frame].size.height;
431     [txt setFrame:rect];
432   }
433
434   // Now put a "Choose" button next to it.
435   //
436   rect.origin.x = rect.origin.y = 0;    
437   rect.size.width = rect.size.height = 10;
438   NSButton *choose = [[NSButton alloc] initWithFrame:rect];
439   [choose setTitle:@"Choose..."];
440   [choose setBezelStyle:NSRoundedBezelStyle];
441   [choose sizeToFit];
442
443   place_child (parent, choose, YES);
444
445   // center the Choose button around the midpoint of the text field.
446   rect = [choose frame];
447   rect.origin.y = ([txt frame].origin.y + 
448                    (([txt frame].size.height - rect.size.height) / 2));
449   [choose setFrameOrigin:rect.origin];
450
451   [choose setTarget:[parent window]];
452   if (dirs_only_p)
453     [choose setAction:@selector(chooseClickedDirs:)];
454   else
455     [choose setAction:@selector(chooseClicked:)];
456
457   [choose release];
458 }
459
460
461 /* Runs a modal file selector and sets the text field's value to the
462    selected file or directory.
463  */
464 static void
465 do_file_selector (NSTextField *txt, BOOL dirs_p)
466 {
467   NSOpenPanel *panel = [NSOpenPanel openPanel];
468   [panel setAllowsMultipleSelection:NO];
469   [panel setCanChooseFiles:!dirs_p];
470   [panel setCanChooseDirectories:dirs_p];
471
472   NSString *file = [txt stringValue];
473   if ([file length] <= 0) {
474     file = NSHomeDirectory();
475     if (dirs_p)
476       file = [file stringByAppendingPathComponent:@"Pictures"];
477   }
478
479 //  NSString *dir = [file stringByDeletingLastPathComponent];
480
481   int result = [panel runModalForDirectory:file //dir
482                                       file:nil //[file lastPathComponent]
483                                      types:nil];
484   if (result == NSOKButton) {
485     NSArray *files = [panel filenames];
486     NSString *file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
487     file = [file stringByAbbreviatingWithTildeInPath];
488     [txt setStringValue:file];
489
490     // Fuck me!  Just setting the value of the NSTextField does not cause
491     // that to end up in the preferences!
492     //
493     NSDictionary *dict = [txt infoForBinding:@"value"];
494     NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
495     NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
496     if ([path hasPrefix:@"values."])  // WTF.
497       path = [path substringFromIndex:7];
498     [[prefs values] setValue:file forKey:path];
499
500 #if 0
501     // make sure the end of the string is visible.
502     NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
503     NSRange range;
504     range.location = [file length]-3;
505     range.length = 1;
506     if (! [[txt window] makeFirstResponder:[txt window]])
507       [[txt window] endEditingFor:nil];
508 //    [[txt window] makeFirstResponder:nil];
509     [fe setSelectedRange:range];
510 //    [tv scrollRangeToVisible:range];
511 //    [txt setNeedsDisplay:YES];
512 //    [[txt cell] setNeedsDisplay:YES];
513 //    [txt selectAll:txt];
514 #endif
515   }
516 }
517
518 /* Returns the NSTextField that is to the left of or above the NSButton.
519  */
520 static NSTextField *
521 find_text_field_of_button (NSButton *button)
522 {
523   NSView *parent = [button superview];
524   NSArray *kids = [parent subviews];
525   int nkids = [kids count];
526   int i;
527   NSTextField *f = 0;
528   for (i = 0; i < nkids; i++) {
529     NSObject *kid = [kids objectAtIndex:i];
530     if ([kid isKindOfClass:[NSTextField class]]) {
531       f = (NSTextField *) kid;
532     } else if (kid == button) {
533       if (! f) abort();
534       return f;
535     }
536   }
537   abort();
538 }
539
540
541 - (void) chooseClicked:(NSObject *)arg
542 {
543   NSButton *choose = (NSButton *) arg;
544   NSTextField *txt = find_text_field_of_button (choose);
545   do_file_selector (txt, NO);
546 }
547
548 - (void) chooseClickedDirs:(NSObject *)arg
549 {
550   NSButton *choose = (NSButton *) arg;
551   NSTextField *txt = find_text_field_of_button (choose);
552   do_file_selector (txt, YES);
553 }
554
555
556 /* Creates the number selection control described by the given XML node.
557    If "type=slider", it's an NSSlider.
558    If "type=spinbutton", it's a text field with up/down arrows next to it.
559 */
560 static void
561 make_number_selector (NSUserDefaultsController *prefs,
562                       const XrmOptionDescRec *opts, 
563                       NSView *parent, NSXMLNode *node)
564 {
565   NSMutableDictionary *dict =
566   [NSMutableDictionary dictionaryWithObjectsAndKeys:
567     @"", @"id",
568     @"", @"_label",
569     @"", @"_low-label",
570     @"", @"_high-label",
571     @"", @"type",
572     @"", @"arg",
573     @"", @"low",
574     @"", @"high",
575     @"", @"default",
576     @"", @"convert",
577     nil];
578   parse_attrs (dict, node);
579   NSString *label      = [dict objectForKey:@"_label"];
580   NSString *low_label  = [dict objectForKey:@"_low-label"];
581   NSString *high_label = [dict objectForKey:@"_high-label"];
582   NSString *type       = [dict objectForKey:@"type"];
583   NSString *arg        = [dict objectForKey:@"arg"];
584   NSString *low        = [dict objectForKey:@"low"];
585   NSString *high       = [dict objectForKey:@"high"];
586   NSString *def        = [dict objectForKey:@"default"];
587   NSString *cvt        = [dict objectForKey:@"convert"];
588   
589   NSAssert1 (arg,  @"no arg in %@", label);
590   NSAssert1 (type, @"no type in %@", label);
591
592   if (! low) {
593     NSAssert1 (0, @"no low in %@", [node name]);
594     return;
595   }
596   if (! high) {
597     NSAssert1 (0, @"no high in %@", [node name]);
598     return;
599   }
600   if (! def) {
601     NSAssert1 (0, @"no default in %@", [node name]);
602     return;
603   }
604   if (cvt && ![cvt isEqualToString:@"invert"]) {
605     NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
606                label);
607   }
608     
609   if ([type isEqualToString:@"slider"]) {
610
611     NSRect rect;
612     rect.origin.x = rect.origin.y = 0;    
613     rect.size.width = 150;
614     rect.size.height = 20;
615     NSSlider *slider;
616     if (cvt)
617       slider = [[InvertedSlider alloc] initWithFrame:rect];
618     else
619       slider = [[NSSlider alloc] initWithFrame:rect];
620
621     [slider setMaxValue:[high doubleValue]];
622     [slider setMinValue:[low  doubleValue]];
623     
624     if (label) {
625       NSTextField *lab = make_label (label);
626       place_child (parent, lab, NO);
627       [lab release];
628     }
629     
630     if (low_label) {
631       NSTextField *lab = make_label (low_label);
632       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
633       [lab setAlignment:1];  // right aligned
634       rect = [lab frame];
635       if (rect.size.width < LEFT_LABEL_WIDTH)
636         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
637       rect.size.height = [slider frame].size.height;
638       [lab setFrame:rect];
639       place_child (parent, lab, NO);
640       [lab release];
641      }
642     
643     place_child (parent, slider, (low_label ? YES : NO));
644     
645     if (! low_label) {
646       rect = [slider frame];
647       if (rect.origin.x < LEFT_LABEL_WIDTH)
648         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
649       [slider setFrame:rect];
650     }
651         
652     if (high_label) {
653       NSTextField *lab = make_label (high_label);
654       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
655       rect = [lab frame];
656       rect.size.height = [slider frame].size.height;
657       [lab setFrame:rect];
658       place_child (parent, lab, YES);
659       [lab release];
660      }
661
662     bind_switch_to_preferences (prefs, slider, arg, opts);
663     [slider release];
664     
665   } else if ([type isEqualToString:@"spinbutton"]) {
666
667     if (! label) {
668       NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
669       return;
670     }
671     NSAssert1 (!low_label,
672               @"low-label not allowed in spinbutton \"%@\"", [node name]);
673     NSAssert1 (!high_label,
674                @"high-label not allowed in spinbutton \"%@\"", [node name]);
675     NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
676                [node name]);
677     
678     NSRect rect;
679     rect.origin.x = rect.origin.y = 0;    
680     rect.size.width = rect.size.height = 10;
681     
682     NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
683     [txt setStringValue:@"0000.0"];
684     [txt sizeToFit];
685     [txt setStringValue:@""];
686     
687     if (label) {
688       NSTextField *lab = make_label (label);
689       //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
690       [lab setAlignment:1];  // right aligned
691       rect = [lab frame];
692       if (rect.size.width < LEFT_LABEL_WIDTH)
693         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
694       rect.size.height = [txt frame].size.height;
695       [lab setFrame:rect];
696       place_child (parent, lab, NO);
697       [lab release];
698      }
699     
700     place_child (parent, txt, (label ? YES : NO));
701     
702     if (! label) {
703       rect = [txt frame];
704       if (rect.origin.x < LEFT_LABEL_WIDTH)
705         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
706       [txt setFrame:rect];
707     }
708     
709     rect.size.width = rect.size.height = 10;
710     NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
711     [step sizeToFit];
712     place_child (parent, step, YES);
713     rect = [step frame];
714     rect.size.height = [txt frame].size.height;
715     rect.origin.x -= COLUMN_SPACING;  // this one goes close
716     [step setFrame:rect];
717     
718     [step setMinValue:[low  doubleValue]];
719     [step setMaxValue:[high doubleValue]];
720     [step setAutorepeat:YES];
721     [step setValueWraps:NO];
722     
723     double range = [high doubleValue] - [low doubleValue];
724     if (range < 1.0)
725       [step setIncrement:range / 10.0];
726     else if (range >= 500)
727       [step setIncrement:range / 100.0];
728     else
729       [step setIncrement:1.0];
730
731     bind_switch_to_preferences (prefs, step, arg, opts);
732     bind_switch_to_preferences (prefs, txt,  arg, opts);
733     
734     [step release];
735     [txt release];
736     
737   } else {
738     NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
739   }
740 }
741
742
743 static void
744 set_menu_item_object (NSMenuItem *item, NSObject *obj)
745 {
746   /* If the object associated with this menu item looks like a boolean,
747      store an NSNumber instead of an NSString, since that's what
748      will be in the preferences (due to similar logic in PrefsReader).
749    */
750   if ([obj isKindOfClass:[NSString class]]) {
751     NSString *string = (NSString *) obj;
752     if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
753         NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
754       obj = [NSNumber numberWithBool:YES];
755     else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
756              NSOrderedSame == [string caseInsensitiveCompare:@"no"])
757       obj = [NSNumber numberWithBool:NO];
758     else
759       obj = string;
760   }
761
762   [item setRepresentedObject:obj];
763   //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
764 }
765
766
767 /* Creates the popup menu described by the given XML node (and its children).
768 */
769 static void
770 make_option_menu (NSUserDefaultsController *prefs,
771                   const XrmOptionDescRec *opts, 
772                   NSView *parent, NSXMLNode *node)
773 {
774   NSArray *children = [node children];
775   int i, count = [children count];
776
777   if (count <= 0) {
778     NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
779     return;
780   }
781
782   // get the "id" attribute off the <select> tag.
783   //
784   NSMutableDictionary *dict =
785     [NSMutableDictionary dictionaryWithObjectsAndKeys:
786       @"", @"id",
787       nil];
788   parse_attrs (dict, node);
789   
790   NSRect rect;
791   rect.origin.x = rect.origin.y = 0;
792   rect.size.width = 10;
793   rect.size.height = 10;
794   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
795                                                      pullsDown:NO];
796
797   NSMenuItem *def_item = nil;
798   float max_width = 0;
799   
800   NSString *menu_key = nil;   // the resource key used by items in this menu
801   
802   for (i = 0; i < count; i++) {
803     NSXMLNode *child = [children objectAtIndex:i];
804
805     if ([child kind] == NSXMLCommentKind)
806       continue;
807     if ([child kind] != NSXMLElementKind) {
808       NSAssert2 (0, @"weird XML node kind: %d: %@", [child kind], node);
809       continue;
810     }
811
812     // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
813     //
814     NSMutableDictionary *dict2 =
815       [NSMutableDictionary dictionaryWithObjectsAndKeys:
816         @"", @"id",
817         @"", @"_label",
818         @"", @"arg-set",
819         nil];
820     parse_attrs (dict2, child);
821     NSString *label   = [dict2 objectForKey:@"_label"];
822     NSString *arg_set = [dict2 objectForKey:@"arg-set"];
823     
824     if (!label) {
825       NSAssert1 (0, @"no _label in %@", [child name]);
826       return;
827     }
828
829     // create the menu item (and then get a pointer to it)
830     [popup addItemWithTitle:label];
831     NSMenuItem *item = [popup itemWithTitle:label];
832
833     if (arg_set) {
834       NSString *this_val = NULL;
835       NSString *this_key = switch_to_resource (arg_set, opts, &this_val);
836       NSAssert1 (this_val, @"this_val null for %@", arg_set);
837       if (menu_key && ![menu_key isEqualToString:this_key])
838         NSAssert3 (0,
839                    @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
840                    menu_key, this_key, this_val);
841       if (this_key)
842         menu_key = this_key;
843
844       /* If this menu has the cmd line "-mode foo" then set this item's
845          value to "foo" (the menu itself will be bound to e.g. "modeString")
846        */
847       set_menu_item_object (item, this_val);
848
849     } else {
850       // no arg-set -- only one menu item can be missing that.
851       NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
852       def_item = item;
853     }
854
855     /* make sure the menu button has room for the text of this item,
856        and remember the greatest width it has reached.
857      */
858     [popup setTitle:label];
859     [popup sizeToFit];
860     NSRect r = [popup frame];
861     if (r.size.width > max_width) max_width = r.size.width;
862   }
863   
864   if (!menu_key) {
865     NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
866     abort();
867   }
868
869   /* We've added all of the menu items.  If there was an item with no
870      command-line switch, then it's the item that represents the default
871      value.  Now we must bind to that item as well...  (We have to bind
872      this one late, because if it was the first item, then we didn't
873      yet know what resource was associated with this menu.)
874    */
875   if (def_item) {
876     NSDictionary *defs = [prefs initialValues];
877     NSObject *def_obj = [defs objectForKey:menu_key];
878
879     NSAssert2 (def_obj, 
880                @"no default value for resource \"%@\" in menu item \"%@\"",
881                menu_key, [def_item title]);
882
883     set_menu_item_object (def_item, def_obj);
884   }
885
886   /* Finish tweaking the menu button itself.
887    */
888   if (def_item)
889     [popup setTitle:[def_item title]];
890   NSRect r = [popup frame];
891   r.size.width = max_width;
892   [popup setFrame:r];
893   place_child (parent, popup, NO);
894
895   bind_resource_to_preferences (prefs, popup, menu_key, opts);
896   [popup release];
897 }
898
899
900 static NSString *unwrap (NSString *);
901 static void hreffify (NSText *);
902 static void boldify (NSText *);
903
904 /* Creates an uneditable, wrapping NSTextField to display the given
905    text enclosed by <description> ... </description> in the XML.
906  */
907 static void
908 make_desc_label (NSView *parent, NSXMLNode *node)
909 {
910   NSString *text = nil;
911   NSArray *children = [node children];
912   int i, count = [children count];
913
914   for (i = 0; i < count; i++) {
915     NSXMLNode *child = [children objectAtIndex:i];
916     NSString *s = [child objectValue];
917     if (text)
918       text = [text stringByAppendingString:s];
919     else
920       text = s;
921   }
922   
923   text = unwrap (text);
924   
925   NSRect rect = [parent frame];
926   rect.origin.x = rect.origin.y = 0;
927   rect.size.width = 200;
928   rect.size.height = 50;  // sized later
929   NSText *lab = [[NSText alloc] initWithFrame:rect];
930   [lab setEditable:NO];
931   [lab setDrawsBackground:NO];
932   [lab setHorizontallyResizable:YES];
933   [lab setVerticallyResizable:YES];
934   [lab setString:text];
935   hreffify (lab);
936   boldify (lab);
937   [lab sizeToFit];
938
939   place_child (parent, lab, NO);
940   [lab release];
941 }
942
943 static NSString *
944 unwrap (NSString *text)
945 {
946   // Unwrap lines: delete \n but do not delete \n\n.
947   //
948   NSArray *lines = [text componentsSeparatedByString:@"\n"];
949   int nlines = [lines count];
950   BOOL eolp = YES;
951   int i;
952
953   text = @"\n";      // start with one blank line
954
955   // skip trailing blank lines in file
956   for (i = nlines-1; i > 0; i--) {
957     NSString *s = (NSString *) [lines objectAtIndex:i];
958     if ([s length] > 0)
959       break;
960     nlines--;
961   }
962
963   // skip leading blank lines in file
964   for (i = 0; i < nlines; i++) {
965     NSString *s = (NSString *) [lines objectAtIndex:i];
966     if ([s length] > 0)
967       break;
968   }
969
970   // unwrap
971   Bool any = NO;
972   for (; i < nlines; i++) {
973     NSString *s = (NSString *) [lines objectAtIndex:i];
974     if ([s length] == 0) {
975       text = [text stringByAppendingString:@"\n\n"];
976       eolp = YES;
977     } else if ([s characterAtIndex:0] == ' ' ||
978                [s hasPrefix:@"Copyright "] ||
979                [s hasPrefix:@"http://"]) {
980       // don't unwrap if the following line begins with whitespace,
981       // or with the word "Copyright", or if it begins with a URL.
982       if (any && !eolp)
983         text = [text stringByAppendingString:@"\n"];
984       text = [text stringByAppendingString:s];
985       any = YES;
986       eolp = NO;
987     } else {
988       if (!eolp)
989         text = [text stringByAppendingString:@" "];
990       text = [text stringByAppendingString:s];
991       eolp = NO;
992       any = YES;
993     }
994   }
995
996   return text;
997 }
998
999
1000 /* Converts any http: URLs in the given text field to clickable links.
1001  */
1002 static void
1003 hreffify (NSText *nstext)
1004 {
1005   NSString *text = [nstext string];
1006   [nstext setRichText:YES];
1007
1008   int L = [text length];
1009   NSRange start;                // range is start-of-search to end-of-string
1010   start.location = 0;
1011   start.length = L;
1012   while (start.location < L) {
1013
1014     // Find the beginning of a URL...
1015     //
1016     NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1017     if (r2.location == NSNotFound)
1018       break;
1019
1020     // Next time around, start searching after this.
1021     start.location = r2.location + r2.length;
1022     start.length = L - start.location;
1023
1024     // Find the end of a URL (whitespace or EOF)...
1025     //
1026     NSRange r3 = [text rangeOfCharacterFromSet:
1027                          [NSCharacterSet whitespaceAndNewlineCharacterSet]
1028                        options:0 range:start];
1029     if (r3.location == NSNotFound)    // EOF
1030       r3.location = L, r3.length = 0;
1031
1032     // Next time around, start searching after this.
1033     start.location = r3.location;
1034     start.length = L - start.location;
1035
1036     // Set r2 to the start/length of this URL.
1037     r2.length = start.location - r2.location;
1038
1039     // Extract the URL.
1040     NSString *nsurl = [text substringWithRange:r2];
1041     const char *url = [nsurl UTF8String];
1042
1043     // Construct the RTF corresponding to <A HREF="url">url</A>
1044     //
1045     const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1046     char *rtf = malloc (strlen (fmt) + (strlen (url) * 2) + 10);
1047     sprintf (rtf, fmt, url, url);
1048     NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1049
1050     // Insert the RTF into the NSText.
1051     [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1052   }
1053 }
1054
1055 /* Makes the text up to the first comma be bold.
1056  */
1057 static void
1058 boldify (NSText *nstext)
1059 {
1060   NSString *text = [nstext string];
1061   NSRange r = [text rangeOfString:@"," options:0];
1062   r.length = r.location+1;
1063   r.location = 0;
1064
1065   NSFont *font = [nstext font];
1066   font = [NSFont boldSystemFontOfSize:[font pointSize]];
1067   [nstext setFont:font range:r];
1068 }
1069
1070
1071 static void layout_group (NSView *group, BOOL horiz_p);
1072
1073
1074 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
1075    wrapped in <hgroup> or <vgroup> in the XML.
1076  */
1077 static void
1078 make_group (NSUserDefaultsController *prefs,
1079             const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, 
1080             BOOL horiz_p)
1081 {
1082   NSRect rect;
1083   rect.size.width = rect.size.height = 1;
1084   rect.origin.x = rect.origin.y = 0;
1085   NSView *group = [[NSView alloc] initWithFrame:rect];
1086   traverse_children (prefs, opts, group, node);
1087
1088   layout_group (group, horiz_p);
1089
1090   rect.size.width = rect.size.height = 0;
1091   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1092   [box setTitlePosition:NSNoTitle];
1093   [box setBorderType:NSNoBorder];
1094   [box setContentViewMargins:rect.size];
1095   [box setContentView:group];
1096   [box sizeToFit];
1097
1098   place_child (parent, box, NO);
1099 }
1100
1101
1102 static void
1103 layout_group (NSView *group, BOOL horiz_p)
1104 {
1105   NSArray *kids = [group subviews];
1106   int nkids = [kids count];
1107   int i;
1108   double maxx = 0, miny = 0;
1109   for (i = 0; i < nkids; i++) {
1110     NSView *kid = [kids objectAtIndex:i];
1111     NSRect r = [kid frame];
1112     
1113     if (horiz_p) {
1114       maxx += r.size.width + COLUMN_SPACING;
1115       if (r.size.height > -miny) miny = -r.size.height;
1116     } else {
1117       if (r.size.width > maxx)  maxx = r.size.width;
1118       miny = r.origin.y - r.size.height;
1119     }
1120   }
1121   
1122   NSRect rect;
1123   rect.size.width = maxx;
1124   rect.size.height = -miny;
1125   [group setFrame:rect];
1126
1127   double x = 0;
1128   for (i = 0; i < nkids; i++) {
1129     NSView *kid = [kids objectAtIndex:i];
1130     NSRect r = [kid frame];
1131     if (horiz_p) {
1132       r.origin.y = rect.size.height - r.size.height;
1133       r.origin.x = x;
1134       x += r.size.width + COLUMN_SPACING;
1135     } else {
1136       r.origin.y -= miny;
1137     }
1138     [kid setFrame:r];
1139   }
1140 }
1141
1142
1143 static void
1144 make_text_controls (NSUserDefaultsController *prefs,
1145                     const XrmOptionDescRec *opts, 
1146                     NSView *parent, NSXMLNode *node)
1147 {
1148   /*
1149     Display Text:
1150      (x)  Computer Name and Time
1151      ( )  Text       [__________________________]
1152      ( )  Text file  [_________________] [Choose]
1153      ( )  URL        [__________________________]
1154
1155     textMode -text-mode date
1156     textMode -text-mode literal   textLiteral -text-literal %
1157     textMode -text-mode file      textFile    -text-file %
1158     textMode -text-mode url       textURL     -text-url %
1159    */
1160   NSRect rect;
1161   rect.size.width = rect.size.height = 1;
1162   rect.origin.x = rect.origin.y = 0;
1163   NSView *group = [[NSView alloc] initWithFrame:rect];
1164   NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1165
1166
1167   NSXMLElement *node2;
1168   NSView *control;
1169
1170   // This is how you link radio buttons together.
1171   //
1172   NSButtonCell *proto = [[NSButtonCell alloc] init];
1173   [proto setButtonType:NSRadioButton];
1174
1175   rect.origin.x = rect.origin.y = 0;
1176   rect.size.width = rect.size.height = 10;
1177   NSMatrix *matrix = [[NSMatrix alloc] 
1178                        initWithFrame:rect
1179                        mode:NSRadioModeMatrix
1180                        prototype:proto
1181                        numberOfRows:4
1182                        numberOfColumns:1];
1183   [matrix setAllowsEmptySelection:NO];
1184
1185   NSArrayController *cnames  = [[NSArrayController alloc] initWithContent:nil];
1186   [cnames addObject:@"Computer Name and Time"];
1187   [cnames addObject:@"Text"];
1188   [cnames addObject:@"File"];
1189   [cnames addObject:@"URL"];
1190   [matrix bind:@"content"
1191           toObject:cnames
1192           withKeyPath:@"arrangedObjects"
1193           options:nil];
1194   [cnames release];
1195
1196   bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts);
1197
1198   place_child (group, matrix, NO);
1199   place_child (group, rgroup, YES);
1200
1201   //  <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1202   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1203   [node2 setAttributesAsDictionary:
1204           [NSDictionary dictionaryWithObjectsAndKeys:
1205                         @"textLiteral",        @"id",
1206                         @"-text-literal %",    @"arg",
1207                         nil]];
1208   make_text_field (prefs, opts, rgroup, node2, YES);
1209   [node2 release];
1210
1211   rect = [last_child(rgroup) frame];
1212
1213 /* // trying to make the text fields be enabled only when the checkbox is on..
1214   control = last_child (rgroup);
1215   [control bind:@"enabled"
1216            toObject:[matrix cellAtRow:1 column:0]
1217            withKeyPath:@"value"
1218            options:nil];
1219 */
1220
1221
1222   //  <file id="textFile" _label="" arg-set="-text-file %"/>
1223   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1224   [node2 setAttributesAsDictionary:
1225           [NSDictionary dictionaryWithObjectsAndKeys:
1226                         @"textFile",           @"id",
1227                         @"-text-file %",       @"arg",
1228                         nil]];
1229   make_file_selector (prefs, opts, rgroup, node2, NO, YES);
1230   [node2 release];
1231
1232   rect = [last_child(rgroup) frame];
1233
1234   //  <string id="textURL" _label="" arg-set="text-url %"/>
1235   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1236   [node2 setAttributesAsDictionary:
1237           [NSDictionary dictionaryWithObjectsAndKeys:
1238                         @"textURL",            @"id",
1239                         @"-text-url %",        @"arg",
1240                         nil]];
1241   make_text_field (prefs, opts, rgroup, node2, YES);
1242   [node2 release];
1243
1244   rect = [last_child(rgroup) frame];
1245
1246   layout_group (rgroup, NO);
1247
1248   rect = [rgroup frame];
1249   rect.size.width += 35;    // WTF?  Why is rgroup too narrow?
1250   [rgroup setFrame:rect];
1251
1252
1253   // Set the height of the cells in the radio-box matrix to the height of
1254   // the (last of the) text fields.
1255   control = last_child (rgroup);
1256   rect = [control frame];
1257   rect.size.width = 30;  // width of the string "Text", plus a bit...
1258   rect.size.height += LINE_SPACING;
1259   [matrix setCellSize:rect.size];
1260   [matrix sizeToCells];
1261
1262   layout_group (group, YES);
1263   rect = [matrix frame];
1264   rect.origin.x += rect.size.width + COLUMN_SPACING;
1265   rect.origin.y -= [control frame].size.height - LINE_SPACING;
1266   [rgroup setFrameOrigin:rect.origin];
1267
1268   // now cheat on the size of the matrix: allow it to overlap (underlap)
1269   // the text fields.
1270   // 
1271   rect.size = [matrix cellSize];
1272   rect.size.width *= 10;
1273   [matrix setCellSize:rect.size];
1274   [matrix sizeToCells];
1275
1276   // Cheat on the position of the stuff on the right (the rgroup).
1277   // GAAAH, this code is such crap!
1278   rect = [rgroup frame];
1279   rect.origin.y -= 5;
1280   [rgroup setFrame:rect];
1281
1282
1283   rect.size.width = rect.size.height = 0;
1284   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1285   [box setTitlePosition:NSAtTop];
1286   [box setBorderType:NSBezelBorder];
1287   [box setTitle:@"Display Text"];
1288
1289   rect.size.width = rect.size.height = 12;
1290   [box setContentViewMargins:rect.size];
1291   [box setContentView:group];
1292   [box sizeToFit];
1293
1294   place_child (parent, box, NO);
1295 }
1296
1297
1298 static void
1299 make_image_controls (NSUserDefaultsController *prefs,
1300                      const XrmOptionDescRec *opts, 
1301                      NSView *parent, NSXMLNode *node)
1302 {
1303   /*
1304     [x]  Grab Desktop Images
1305     [ ]  Choose Random Image:
1306          [__________________________]  [Choose]
1307
1308    <boolean id="grabDesktopImages" _label="Grab Desktop Images"
1309        arg-unset="-no-grab-desktop"/>
1310    <boolean id="chooseRandomImages" _label="Grab Desktop Images"
1311        arg-unset="-choose-random-images"/>
1312    <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
1313    */
1314
1315   NSXMLElement *node2;
1316
1317   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1318   [node2 setAttributesAsDictionary:
1319           [NSDictionary dictionaryWithObjectsAndKeys:
1320                         @"grabDesktopImages",   @"id",
1321                         @"Grab Desktop Images", @"_label",
1322                         @"-no-grab-desktop",    @"arg-unset",
1323                         nil]];
1324   make_checkbox (prefs, opts, parent, node2);
1325   [node2 release];
1326
1327   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1328   [node2 setAttributesAsDictionary:
1329           [NSDictionary dictionaryWithObjectsAndKeys:
1330                         @"chooseRandomImages",    @"id",
1331                         @"Choose Random Images",  @"_label",
1332                         @"-choose-random-images", @"arg-set",
1333                         nil]];
1334   make_checkbox (prefs, opts, parent, node2);
1335   [node2 release];
1336
1337   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1338   [node2 setAttributesAsDictionary:
1339           [NSDictionary dictionaryWithObjectsAndKeys:
1340                         @"imageDirectory",     @"id",
1341                         @"Images Directory:",  @"_label",
1342                         @"-image-directory %", @"arg",
1343                         nil]];
1344   make_file_selector (prefs, opts, parent, node2, YES, NO);
1345   [node2 release];
1346 }
1347
1348
1349
1350 /* Create some kind of control corresponding to the given XML node.
1351  */
1352 static void
1353 make_control (NSUserDefaultsController *prefs,
1354               const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
1355 {
1356   NSString *name = [node name];
1357
1358   if ([node kind] == NSXMLCommentKind)
1359     return;
1360   if ([node kind] != NSXMLElementKind) {
1361     NSAssert2 (0, @"weird XML node kind: %d: %@", [node kind], node);
1362     return;
1363   }
1364
1365   if ([name isEqualToString:@"hgroup"] ||
1366       [name isEqualToString:@"vgroup"]) {
1367
1368     BOOL horiz_p = [name isEqualToString:@"hgroup"];
1369     make_group (prefs, opts, parent, node, horiz_p);
1370
1371   } else if ([name isEqualToString:@"command"]) {
1372     // do nothing: this is the "-root" business
1373
1374   } else if ([name isEqualToString:@"boolean"]) {
1375     make_checkbox (prefs, opts, parent, node);
1376
1377   } else if ([name isEqualToString:@"string"]) {
1378     make_text_field (prefs, opts, parent, node, NO);
1379
1380   } else if ([name isEqualToString:@"file"]) {
1381     make_file_selector (prefs, opts, parent, node, NO, NO);
1382
1383   } else if ([name isEqualToString:@"number"]) {
1384     make_number_selector (prefs, opts, parent, node);
1385
1386   } else if ([name isEqualToString:@"select"]) {
1387     make_option_menu (prefs, opts, parent, node);
1388
1389   } else if ([name isEqualToString:@"_description"]) {
1390     make_desc_label (parent, node);
1391
1392   } else if ([name isEqualToString:@"xscreensaver-text"]) {
1393     make_text_controls (prefs, opts, parent, node);
1394
1395   } else if ([name isEqualToString:@"xscreensaver-image"]) {
1396     make_image_controls (prefs, opts, parent, node);
1397
1398   } else {
1399     NSAssert1 (0, @"unknown tag: %@", name);
1400   }
1401 }
1402
1403
1404 /* Iterate over and process the children of this XML node.
1405  */
1406 static void
1407 traverse_children (NSUserDefaultsController *prefs,
1408                    const XrmOptionDescRec *opts,
1409                    NSView *parent, NSXMLNode *node)
1410 {
1411   NSArray *children = [node children];
1412   int i, count = [children count];
1413   for (i = 0; i < count; i++) {
1414     NSXMLNode *child = [children objectAtIndex:i];
1415     make_control (prefs, opts, parent, child);
1416   }
1417 }
1418
1419 /* Handle the options on the top level <xscreensaver> tag.
1420  */
1421 static void
1422 parse_xscreensaver_tag (NSXMLNode *node)
1423 {
1424   NSMutableDictionary *dict =
1425   [NSMutableDictionary dictionaryWithObjectsAndKeys:
1426     @"", @"name",
1427     @"", @"_label",
1428     nil];
1429   parse_attrs (dict, node);
1430   NSString *name  = [dict objectForKey:@"name"];
1431   NSString *label = [dict objectForKey:@"_label"];
1432     
1433   if (!label) {
1434     NSAssert1 (0, @"no _label in %@", [node name]);
1435     return;
1436   }
1437   if (!name) {
1438     NSAssert1 (0, @"no name in \"%@\"", label);
1439     return;
1440   }
1441   
1442   // #### do any callers need the "name" field for anything?
1443 }
1444
1445
1446 /* Kludgey magic to make the window enclose the controls we created.
1447  */
1448 static void
1449 fix_contentview_size (NSView *parent)
1450 {
1451   NSRect f;
1452   NSArray *kids = [parent subviews];
1453   int nkids = [kids count];
1454   NSView *text;  // the NSText at the bottom of the window
1455   NSView *last;  // the last child before the NSText
1456   double maxx = 0, miny = 0;
1457   int i;
1458
1459   /* Find the size of the rectangle taken up by each of the children
1460      except the final "NSText" child.
1461   */
1462   for (i = 0; i < nkids; i++) {
1463     NSView *kid = [kids objectAtIndex:i];
1464     if ([kid isKindOfClass:[NSText class]]) {
1465       text = kid;
1466       continue;
1467     }
1468     f = [kid frame];
1469     if (f.origin.x + f.size.width > maxx)  maxx = f.origin.x + f.size.width;
1470     if (f.origin.y - f.size.height < miny) miny = f.origin.y;
1471     last = kid;
1472 //    NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1473 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1474 //          f.origin.y + f.size.height, [kid class]);
1475   }
1476   
1477   if (maxx < 400) maxx = 400;   // leave room for the NSText paragraph...
1478   
1479   /* Now that we know the width of the window, set the width of the NSText to
1480      that, so that it can decide what its height needs to be.
1481    */
1482   f = [text frame];
1483 //  NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1484 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1485 //        f.origin.y + f.size.height, [text class]);
1486   
1487   // set the NSText's width (this changes its height).
1488   f.size.width = maxx - LEFT_MARGIN;
1489   [text setFrame:f];
1490   
1491   // position the NSText below the last child (this gives us a new miny).
1492   f = [text frame];
1493   f.origin.y = miny - f.size.height - LINE_SPACING;
1494   miny = f.origin.y - LINE_SPACING;
1495   [text setFrame:f];
1496   
1497   // Lock the width of the field and unlock the height, and let it resize
1498   // once more, to compute the proper height of the text for that width.
1499   //
1500   [(NSText *) text setHorizontallyResizable:NO];
1501   [(NSText *) text setVerticallyResizable:YES];
1502   [(NSText *) text sizeToFit];
1503
1504   // Now lock the height too: no more resizing this text field.
1505   //
1506   [(NSText *) text setVerticallyResizable:NO];
1507
1508   // Now reposition the top edge of the text field to be back where it
1509   // was before we changed the height.
1510   //
1511   float oh = f.size.height;
1512   f = [text frame];
1513   float dh = f.size.height - oh;
1514   f.origin.y += dh;
1515   [text setFrame:f];
1516
1517   // Also adjust the parent height by the change in height of the text field.
1518   miny -= dh;
1519
1520 //  NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1521 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1522 //        f.origin.y + f.size.height, [text class]);
1523   
1524   
1525   /* Set the contentView to the size of the children.
1526    */
1527   f = [parent frame];
1528   float yoff = f.size.height;
1529   f.size.width = maxx + LEFT_MARGIN;
1530   f.size.height = -(miny - LEFT_MARGIN*2);
1531   yoff = f.size.height - yoff;
1532   [parent setFrame:f];
1533
1534 //  NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", 
1535 //        f.size.width, f.size.height, f.origin.x, f.origin.y);
1536
1537   /* Now move all of the kids up into the window.
1538    */
1539   f = [parent frame];
1540   float shift = f.size.height;
1541 //  NSLog(@"shift: %3.0f", shift);
1542   for (i = 0; i < nkids; i++) {
1543     NSView *kid = [kids objectAtIndex:i];
1544     f = [kid frame];
1545     f.origin.y += shift;
1546     [kid setFrame:f];
1547 //    NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1548 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1549 //          f.origin.y + f.size.height, [kid class]);
1550   }
1551   
1552   /* Set the kids to track the top left corner of the window when resized.
1553      Set the NSText to track the bottom right corner as well.
1554    */
1555   for (i = 0; i < nkids; i++) {
1556     NSView *kid = [kids objectAtIndex:i];
1557     unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
1558     if ([kid isKindOfClass:[NSText class]])
1559       mask |= NSViewWidthSizable|NSViewHeightSizable;
1560     [kid setAutoresizingMask:mask];
1561   }
1562 }
1563
1564
1565 - (void) okClicked:(NSObject *)arg
1566 {
1567   [userDefaultsController commitEditing];
1568   [userDefaultsController save:self];
1569   [NSApp endSheet:self returnCode:NSOKButton];
1570   [self close];
1571 }
1572
1573 - (void) cancelClicked:(NSObject *)arg
1574 {
1575   [userDefaultsController revert:self];
1576   [NSApp endSheet:self returnCode:NSCancelButton];
1577   [self close];
1578 }
1579
1580 - (void) resetClicked:(NSObject *)arg
1581 {
1582   [userDefaultsController revertToInitialValues:self];
1583 }
1584
1585
1586 static NSView *
1587 wrap_with_buttons (NSWindow *window, NSView *panel)
1588 {
1589   NSRect rect;
1590   
1591   // Make a box to hold the buttons at the bottom of the window.
1592   //
1593   rect = [panel frame];
1594   rect.origin.x = rect.origin.y = 0;
1595   rect.size.height = 10;
1596   NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
1597   [bbox setTitlePosition:NSNoTitle];  
1598   [bbox setBorderType:NSNoBorder];
1599   
1600   // Make some buttons: Default, Cancel, OK
1601   //
1602   rect.origin.x = rect.origin.y = 0;
1603   rect.size.width = rect.size.height = 10;
1604   NSButton *reset = [[NSButton alloc] initWithFrame:rect];
1605   [reset setTitle:@"Reset to Defaults"];
1606   [reset setBezelStyle:NSRoundedBezelStyle];
1607   [reset sizeToFit];
1608
1609   rect = [reset frame];
1610   NSButton *ok = [[NSButton alloc] initWithFrame:rect];
1611   [ok setTitle:@"OK"];
1612   [ok setBezelStyle:NSRoundedBezelStyle];
1613   [ok sizeToFit];
1614   rect = [bbox frame];
1615   rect.origin.x = rect.size.width - [ok frame].size.width;
1616   [ok setFrameOrigin:rect.origin];
1617
1618   rect = [ok frame];
1619   NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
1620   [cancel setTitle:@"Cancel"];
1621   [cancel setBezelStyle:NSRoundedBezelStyle];
1622   [cancel sizeToFit];
1623   rect.origin.x -= [cancel frame].size.width + 10;
1624   [cancel setFrameOrigin:rect.origin];
1625
1626   // Bind OK to RET and Cancel to ESC.
1627   [ok     setKeyEquivalent:@"\r"];
1628   [cancel setKeyEquivalent:@"\e"];
1629
1630   // The correct width for OK and Cancel buttons is 68 pixels
1631   // ("Human Interface Guidelines: Controls: Buttons: 
1632   // Push Button Specifications").
1633   //
1634   rect = [ok frame];
1635   rect.size.width = 68;
1636   [ok setFrame:rect];
1637
1638   rect = [cancel frame];
1639   rect.size.width = 68;
1640   [cancel setFrame:rect];
1641
1642   // It puts the buttons in the box or else it gets the hose again
1643   //
1644   [bbox addSubview:ok];
1645   [bbox addSubview:cancel];
1646   [bbox addSubview:reset];
1647   [bbox sizeToFit];
1648   
1649   // make a box to hold the button-box, and the preferences view
1650   //
1651   rect = [bbox frame];
1652   rect.origin.y += rect.size.height;
1653   NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
1654   [pbox setTitlePosition:NSNoTitle];
1655   [pbox setBorderType:NSBezelBorder];
1656
1657   {
1658     NSRect f = [panel frame];
1659     int screen_height = 800 - 64;
1660     if (f.size.height > screen_height) {
1661       NSLog(@"%@ height was %.0f; clipping to %d", 
1662           [panel class], f.size.height, screen_height);
1663       f.size.height = screen_height;
1664       [panel setFrame:f];
1665     }
1666   }
1667
1668   [pbox addSubview:panel];
1669   [pbox addSubview:bbox];
1670   [pbox sizeToFit];
1671
1672   [reset  setAutoresizingMask:NSViewMaxXMargin];
1673   [cancel setAutoresizingMask:NSViewMinXMargin];
1674   [ok     setAutoresizingMask:NSViewMinXMargin];
1675   [bbox   setAutoresizingMask:NSViewWidthSizable];
1676   
1677   // grab the clicks
1678   //
1679   [ok     setTarget:window];
1680   [cancel setTarget:window];
1681   [reset  setTarget:window];
1682   [ok     setAction:@selector(okClicked:)];
1683   [cancel setAction:@selector(cancelClicked:)];
1684   [reset  setAction:@selector(resetClicked:)];
1685   
1686   return pbox;
1687 }
1688
1689
1690 /* Iterate over and process the children of the root node of the XML document.
1691  */
1692 static void
1693 traverse_tree (NSUserDefaultsController *prefs,
1694                NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node)
1695 {
1696   if (![[node name] isEqualToString:@"screensaver"]) {
1697     NSAssert (0, @"top level node is not <xscreensaver>");
1698   }
1699
1700   parse_xscreensaver_tag (node);
1701   
1702   NSRect rect;
1703   rect.origin.x = rect.origin.y = 0;
1704   rect.size.width = rect.size.height = 1;
1705
1706   NSView *panel = [[NSView alloc] initWithFrame:rect];
1707   
1708   traverse_children (prefs, opts, panel, node);
1709   fix_contentview_size (panel);
1710
1711   NSView *root = wrap_with_buttons (window, panel);
1712   [prefs setAppliesImmediately:NO];
1713
1714   [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1715
1716   rect = [window frameRectForContentRect:[root frame]];
1717   [window setFrame:rect display:NO];
1718   [window setMinSize:rect.size];
1719   
1720   [window setContentView:root];
1721 }
1722
1723
1724 /* When this object is instantiated, it parses the XML file and creates
1725    controls on itself that are hooked up to the appropriate preferences.
1726    The default size of the view is just big enough to hold them all.
1727  */
1728 - (id)initWithXMLFile: (NSString *) xml_file
1729               options: (const XrmOptionDescRec *) opts
1730            controller: (NSUserDefaultsController *) prefs
1731 {
1732   if (! (self = [super init]))
1733     return 0;
1734
1735   // instance variable
1736   userDefaultsController = prefs;
1737   [prefs retain];
1738
1739   NSURL *furl = [NSURL fileURLWithPath:xml_file];
1740
1741   if (!furl) {
1742     NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
1743     return nil;
1744   }
1745
1746   NSError *err = nil;
1747   NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] 
1748                             initWithContentsOfURL:furl
1749                             options:(NSXMLNodePreserveWhitespace |
1750                                      NSXMLNodePreserveCDATA)
1751                             error:&err];
1752 /* clean up?
1753     if (!xmlDoc) {
1754       xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl
1755                                       options:NSXMLDocumentTidyXML
1756                                       error:&err];
1757     }
1758 */
1759   if (!xmlDoc || err) {
1760     if (err)
1761       NSAssert2 (0, @"XML Error: %@: %@",
1762                  xml_file, [err localizedDescription]);
1763     return nil;
1764   }
1765
1766   traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
1767
1768   return self;
1769 }
1770
1771
1772 - (void) dealloc
1773 {
1774   [userDefaultsController release];
1775   [super dealloc];
1776 }
1777
1778 @end