http://ftp.nluug.nl/pub/os/Linux/distr/pardusrepo/sources/xscreensaver-5.02.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 first word of the text be bold.
1056  */
1057 static void
1058 boldify (NSText *nstext)
1059 {
1060   NSString *text = [nstext string];
1061   NSRange r = [text rangeOfCharacterFromSet:
1062                       [NSCharacterSet whitespaceCharacterSet]];
1063   r.length = r.location;
1064   r.location = 0;
1065
1066   NSFont *font = [nstext font];
1067   font = [NSFont boldSystemFontOfSize:[font pointSize]];
1068   [nstext setFont:font range:r];
1069 }
1070
1071
1072 static void layout_group (NSView *group, BOOL horiz_p);
1073
1074
1075 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
1076    wrapped in <hgroup> or <vgroup> in the XML.
1077  */
1078 static void
1079 make_group (NSUserDefaultsController *prefs,
1080             const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, 
1081             BOOL horiz_p)
1082 {
1083   NSRect rect;
1084   rect.size.width = rect.size.height = 1;
1085   rect.origin.x = rect.origin.y = 0;
1086   NSView *group = [[NSView alloc] initWithFrame:rect];
1087   traverse_children (prefs, opts, group, node);
1088
1089   layout_group (group, horiz_p);
1090
1091   rect.size.width = rect.size.height = 0;
1092   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1093   [box setTitlePosition:NSNoTitle];
1094   [box setBorderType:NSNoBorder];
1095   [box setContentViewMargins:rect.size];
1096   [box setContentView:group];
1097   [box sizeToFit];
1098
1099   place_child (parent, box, NO);
1100 }
1101
1102
1103 static void
1104 layout_group (NSView *group, BOOL horiz_p)
1105 {
1106   NSArray *kids = [group subviews];
1107   int nkids = [kids count];
1108   int i;
1109   double maxx = 0, miny = 0;
1110   for (i = 0; i < nkids; i++) {
1111     NSView *kid = [kids objectAtIndex:i];
1112     NSRect r = [kid frame];
1113     
1114     if (horiz_p) {
1115       maxx += r.size.width + COLUMN_SPACING;
1116       if (r.size.height > -miny) miny = -r.size.height;
1117     } else {
1118       if (r.size.width > maxx)  maxx = r.size.width;
1119       miny = r.origin.y - r.size.height;
1120     }
1121   }
1122   
1123   NSRect rect;
1124   rect.size.width = maxx;
1125   rect.size.height = -miny;
1126   [group setFrame:rect];
1127
1128   double x = 0;
1129   for (i = 0; i < nkids; i++) {
1130     NSView *kid = [kids objectAtIndex:i];
1131     NSRect r = [kid frame];
1132     if (horiz_p) {
1133       r.origin.y = rect.size.height - r.size.height;
1134       r.origin.x = x;
1135       x += r.size.width + COLUMN_SPACING;
1136     } else {
1137       r.origin.y -= miny;
1138     }
1139     [kid setFrame:r];
1140   }
1141 }
1142
1143
1144 static void
1145 make_text_controls (NSUserDefaultsController *prefs,
1146                     const XrmOptionDescRec *opts, 
1147                     NSView *parent, NSXMLNode *node)
1148 {
1149   /*
1150     Display Text:
1151      (x)  Computer Name and Time
1152      ( )  Text       [__________________________]
1153      ( )  Text file  [_________________] [Choose]
1154      ( )  URL        [__________________________]
1155
1156     textMode -text-mode date
1157     textMode -text-mode literal   textLiteral -text-literal %
1158     textMode -text-mode file      textFile    -text-file %
1159     textMode -text-mode url       textURL     -text-url %
1160    */
1161   NSRect rect;
1162   rect.size.width = rect.size.height = 1;
1163   rect.origin.x = rect.origin.y = 0;
1164   NSView *group = [[NSView alloc] initWithFrame:rect];
1165   NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1166
1167
1168   NSXMLElement *node2;
1169   NSView *control;
1170
1171   // This is how you link radio buttons together.
1172   //
1173   NSButtonCell *proto = [[NSButtonCell alloc] init];
1174   [proto setButtonType:NSRadioButton];
1175
1176   rect.origin.x = rect.origin.y = 0;
1177   rect.size.width = rect.size.height = 10;
1178   NSMatrix *matrix = [[NSMatrix alloc] 
1179                        initWithFrame:rect
1180                        mode:NSRadioModeMatrix
1181                        prototype:proto
1182                        numberOfRows:4
1183                        numberOfColumns:1];
1184   [matrix setAllowsEmptySelection:NO];
1185
1186   NSArrayController *cnames  = [[NSArrayController alloc] initWithContent:nil];
1187   [cnames addObject:@"Computer Name and Time"];
1188   [cnames addObject:@"Text"];
1189   [cnames addObject:@"File"];
1190   [cnames addObject:@"URL"];
1191   [matrix bind:@"content"
1192           toObject:cnames
1193           withKeyPath:@"arrangedObjects"
1194           options:nil];
1195   [cnames release];
1196
1197   bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts);
1198
1199   place_child (group, matrix, NO);
1200   place_child (group, rgroup, YES);
1201
1202   //  <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1203   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1204   [node2 setAttributesAsDictionary:
1205           [NSDictionary dictionaryWithObjectsAndKeys:
1206                         @"textLiteral",        @"id",
1207                         @"-text-literal %",    @"arg",
1208                         nil]];
1209   make_text_field (prefs, opts, rgroup, node2, YES);
1210   [node2 release];
1211
1212   rect = [last_child(rgroup) frame];
1213
1214 /* // trying to make the text fields be enabled only when the checkbox is on..
1215   control = last_child (rgroup);
1216   [control bind:@"enabled"
1217            toObject:[matrix cellAtRow:1 column:0]
1218            withKeyPath:@"value"
1219            options:nil];
1220 */
1221
1222
1223   //  <file id="textFile" _label="" arg-set="-text-file %"/>
1224   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1225   [node2 setAttributesAsDictionary:
1226           [NSDictionary dictionaryWithObjectsAndKeys:
1227                         @"textFile",           @"id",
1228                         @"-text-file %",       @"arg",
1229                         nil]];
1230   make_file_selector (prefs, opts, rgroup, node2, NO, YES);
1231   [node2 release];
1232
1233   rect = [last_child(rgroup) frame];
1234
1235   //  <string id="textURL" _label="" arg-set="text-url %"/>
1236   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1237   [node2 setAttributesAsDictionary:
1238           [NSDictionary dictionaryWithObjectsAndKeys:
1239                         @"textURL",            @"id",
1240                         @"-text-url %",        @"arg",
1241                         nil]];
1242   make_text_field (prefs, opts, rgroup, node2, YES);
1243   [node2 release];
1244
1245   rect = [last_child(rgroup) frame];
1246
1247   layout_group (rgroup, NO);
1248
1249   rect = [rgroup frame];
1250   rect.size.width += 35;    // WTF?  Why is rgroup too narrow?
1251   [rgroup setFrame:rect];
1252
1253
1254   // Set the height of the cells in the radio-box matrix to the height of
1255   // the (last of the) text fields.
1256   control = last_child (rgroup);
1257   rect = [control frame];
1258   rect.size.width = 30;  // width of the string "Text", plus a bit...
1259   rect.size.height += LINE_SPACING;
1260   [matrix setCellSize:rect.size];
1261   [matrix sizeToCells];
1262
1263   layout_group (group, YES);
1264   rect = [matrix frame];
1265   rect.origin.x += rect.size.width + COLUMN_SPACING;
1266   rect.origin.y -= [control frame].size.height - LINE_SPACING;
1267   [rgroup setFrameOrigin:rect.origin];
1268
1269   // now cheat on the size of the matrix: allow it to overlap (underlap)
1270   // the text fields.
1271   // 
1272   rect.size = [matrix cellSize];
1273   rect.size.width *= 10;
1274   [matrix setCellSize:rect.size];
1275   [matrix sizeToCells];
1276
1277   // Cheat on the position of the stuff on the right (the rgroup).
1278   // GAAAH, this code is such crap!
1279   rect = [rgroup frame];
1280   rect.origin.y -= 5;
1281   [rgroup setFrame:rect];
1282
1283
1284   rect.size.width = rect.size.height = 0;
1285   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1286   [box setTitlePosition:NSAtTop];
1287   [box setBorderType:NSBezelBorder];
1288   [box setTitle:@"Display Text"];
1289
1290   rect.size.width = rect.size.height = 12;
1291   [box setContentViewMargins:rect.size];
1292   [box setContentView:group];
1293   [box sizeToFit];
1294
1295   place_child (parent, box, NO);
1296 }
1297
1298
1299 static void
1300 make_image_controls (NSUserDefaultsController *prefs,
1301                      const XrmOptionDescRec *opts, 
1302                      NSView *parent, NSXMLNode *node)
1303 {
1304   /*
1305     [x]  Grab Desktop Images
1306     [ ]  Choose Random Image:
1307          [__________________________]  [Choose]
1308
1309    <boolean id="grabDesktopImages" _label="Grab Desktop Images"
1310        arg-unset="-no-grab-desktop"/>
1311    <boolean id="chooseRandomImages" _label="Grab Desktop Images"
1312        arg-unset="-choose-random-images"/>
1313    <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
1314    */
1315
1316   NSXMLElement *node2;
1317
1318   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1319   [node2 setAttributesAsDictionary:
1320           [NSDictionary dictionaryWithObjectsAndKeys:
1321                         @"grabDesktopImages",   @"id",
1322                         @"Grab Desktop Images", @"_label",
1323                         @"-no-grab-desktop",    @"arg-unset",
1324                         nil]];
1325   make_checkbox (prefs, opts, parent, node2);
1326   [node2 release];
1327
1328   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1329   [node2 setAttributesAsDictionary:
1330           [NSDictionary dictionaryWithObjectsAndKeys:
1331                         @"chooseRandomImages",    @"id",
1332                         @"Choose Random Images",  @"_label",
1333                         @"-choose-random-images", @"arg-set",
1334                         nil]];
1335   make_checkbox (prefs, opts, parent, node2);
1336   [node2 release];
1337
1338   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1339   [node2 setAttributesAsDictionary:
1340           [NSDictionary dictionaryWithObjectsAndKeys:
1341                         @"imageDirectory",     @"id",
1342                         @"Images Directory:",  @"_label",
1343                         @"-image-directory %", @"arg",
1344                         nil]];
1345   make_file_selector (prefs, opts, parent, node2, YES, NO);
1346   [node2 release];
1347 }
1348
1349
1350
1351 /* Create some kind of control corresponding to the given XML node.
1352  */
1353 static void
1354 make_control (NSUserDefaultsController *prefs,
1355               const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
1356 {
1357   NSString *name = [node name];
1358
1359   if ([node kind] == NSXMLCommentKind)
1360     return;
1361   if ([node kind] != NSXMLElementKind) {
1362     NSAssert2 (0, @"weird XML node kind: %d: %@", [node kind], node);
1363     return;
1364   }
1365
1366   if ([name isEqualToString:@"hgroup"] ||
1367       [name isEqualToString:@"vgroup"]) {
1368
1369     BOOL horiz_p = [name isEqualToString:@"hgroup"];
1370     make_group (prefs, opts, parent, node, horiz_p);
1371
1372   } else if ([name isEqualToString:@"command"]) {
1373     // do nothing: this is the "-root" business
1374
1375   } else if ([name isEqualToString:@"boolean"]) {
1376     make_checkbox (prefs, opts, parent, node);
1377
1378   } else if ([name isEqualToString:@"string"]) {
1379     make_text_field (prefs, opts, parent, node, NO);
1380
1381   } else if ([name isEqualToString:@"file"]) {
1382     make_file_selector (prefs, opts, parent, node, NO, NO);
1383
1384   } else if ([name isEqualToString:@"number"]) {
1385     make_number_selector (prefs, opts, parent, node);
1386
1387   } else if ([name isEqualToString:@"select"]) {
1388     make_option_menu (prefs, opts, parent, node);
1389
1390   } else if ([name isEqualToString:@"_description"]) {
1391     make_desc_label (parent, node);
1392
1393   } else if ([name isEqualToString:@"xscreensaver-text"]) {
1394     make_text_controls (prefs, opts, parent, node);
1395
1396   } else if ([name isEqualToString:@"xscreensaver-image"]) {
1397     make_image_controls (prefs, opts, parent, node);
1398
1399   } else {
1400     NSAssert1 (0, @"unknown tag: %@", name);
1401   }
1402 }
1403
1404
1405 /* Iterate over and process the children of this XML node.
1406  */
1407 static void
1408 traverse_children (NSUserDefaultsController *prefs,
1409                    const XrmOptionDescRec *opts,
1410                    NSView *parent, NSXMLNode *node)
1411 {
1412   NSArray *children = [node children];
1413   int i, count = [children count];
1414   for (i = 0; i < count; i++) {
1415     NSXMLNode *child = [children objectAtIndex:i];
1416     make_control (prefs, opts, parent, child);
1417   }
1418 }
1419
1420 /* Handle the options on the top level <xscreensaver> tag.
1421  */
1422 static void
1423 parse_xscreensaver_tag (NSXMLNode *node)
1424 {
1425   NSMutableDictionary *dict =
1426   [NSMutableDictionary dictionaryWithObjectsAndKeys:
1427     @"", @"name",
1428     @"", @"_label",
1429     nil];
1430   parse_attrs (dict, node);
1431   NSString *name  = [dict objectForKey:@"name"];
1432   NSString *label = [dict objectForKey:@"_label"];
1433     
1434   if (!label) {
1435     NSAssert1 (0, @"no _label in %@", [node name]);
1436     return;
1437   }
1438   if (!name) {
1439     NSAssert1 (0, @"no name in \"%@\"", label);
1440     return;
1441   }
1442   
1443   // #### do any callers need the "name" field for anything?
1444 }
1445
1446
1447 /* Kludgey magic to make the window enclose the controls we created.
1448  */
1449 static void
1450 fix_contentview_size (NSView *parent)
1451 {
1452   NSRect f;
1453   NSArray *kids = [parent subviews];
1454   int nkids = [kids count];
1455   NSView *text;  // the NSText at the bottom of the window
1456   NSView *last;  // the last child before the NSText
1457   double maxx = 0, miny = 0;
1458   int i;
1459
1460   /* Find the size of the rectangle taken up by each of the children
1461      except the final "NSText" child.
1462   */
1463   for (i = 0; i < nkids; i++) {
1464     NSView *kid = [kids objectAtIndex:i];
1465     if ([kid isKindOfClass:[NSText class]]) {
1466       text = kid;
1467       continue;
1468     }
1469     f = [kid frame];
1470     if (f.origin.x + f.size.width > maxx)  maxx = f.origin.x + f.size.width;
1471     if (f.origin.y - f.size.height < miny) miny = f.origin.y;
1472     last = kid;
1473 //    NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1474 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1475 //          f.origin.y + f.size.height, [kid class]);
1476   }
1477   
1478   if (maxx < 350) maxx = 350;   // leave room for the NSText paragraph...
1479   
1480   /* Now that we know the width of the window, set the width of the NSText to
1481      that, so that it can decide what its height needs to be.
1482    */
1483   f = [text frame];
1484 //  NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1485 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1486 //        f.origin.y + f.size.height, [text class]);
1487   
1488   // set the NSText's width (this changes its height).
1489   f.size.width = maxx - LEFT_MARGIN;
1490   [text setFrame:f];
1491   
1492   // position the NSText below the last child (this gives us a new miny).
1493   f = [text frame];
1494   f.origin.y = miny - f.size.height - LINE_SPACING;
1495   miny = f.origin.y - LINE_SPACING;
1496   [text setFrame:f];
1497   
1498   // Stop second-guessing us on sizing now.  Size is now locked.
1499   [(NSText *) text setHorizontallyResizable:NO];
1500   [(NSText *) text setVerticallyResizable:NO];
1501   
1502 //  NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1503 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1504 //        f.origin.y + f.size.height, [text class]);
1505   
1506   
1507   /* Set the contentView to the size of the children.
1508    */
1509   f = [parent frame];
1510   float yoff = f.size.height;
1511   f.size.width = maxx + LEFT_MARGIN;
1512   f.size.height = -(miny - LEFT_MARGIN*2);
1513   yoff = f.size.height - yoff;
1514   [parent setFrame:f];
1515
1516 //  NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", 
1517 //        f.size.width, f.size.height, f.origin.x, f.origin.y);
1518
1519   /* Now move all of the kids up into the window.
1520    */
1521   f = [parent frame];
1522   float shift = f.size.height;
1523 //  NSLog(@"shift: %3.0f", shift);
1524   for (i = 0; i < nkids; i++) {
1525     NSView *kid = [kids objectAtIndex:i];
1526     f = [kid frame];
1527     f.origin.y += shift;
1528     [kid setFrame:f];
1529 //    NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1530 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1531 //          f.origin.y + f.size.height, [kid class]);
1532   }
1533   
1534   /* Set the kids to track the top left corner of the window when resized.
1535      Set the NSText to track the bottom right corner as well.
1536    */
1537   for (i = 0; i < nkids; i++) {
1538     NSView *kid = [kids objectAtIndex:i];
1539     unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
1540     if ([kid isKindOfClass:[NSText class]])
1541       mask |= NSViewWidthSizable|NSViewHeightSizable;
1542     [kid setAutoresizingMask:mask];
1543   }
1544 }
1545
1546
1547 - (void) okClicked:(NSObject *)arg
1548 {
1549   [userDefaultsController commitEditing];
1550   [userDefaultsController save:self];
1551   [NSApp endSheet:self returnCode:NSOKButton];
1552   [self close];
1553 }
1554
1555 - (void) cancelClicked:(NSObject *)arg
1556 {
1557   [userDefaultsController revert:self];
1558   [NSApp endSheet:self returnCode:NSCancelButton];
1559   [self close];
1560 }
1561
1562 - (void) resetClicked:(NSObject *)arg
1563 {
1564   [userDefaultsController revertToInitialValues:self];
1565 }
1566
1567
1568 static NSView *
1569 wrap_with_buttons (NSWindow *window, NSView *panel)
1570 {
1571   NSRect rect;
1572   
1573   // Make a box to hold the buttons at the bottom of the window.
1574   //
1575   rect = [panel frame];
1576   rect.origin.x = rect.origin.y = 0;
1577   rect.size.height = 10;
1578   NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
1579   [bbox setTitlePosition:NSNoTitle];  
1580   [bbox setBorderType:NSNoBorder];
1581   
1582   // Make some buttons: Default, Cancel, OK
1583   //
1584   rect.origin.x = rect.origin.y = 0;
1585   rect.size.width = rect.size.height = 10;
1586   NSButton *reset = [[NSButton alloc] initWithFrame:rect];
1587   [reset setTitle:@"Reset to Defaults"];
1588   [reset setBezelStyle:NSRoundedBezelStyle];
1589   [reset sizeToFit];
1590
1591   rect = [reset frame];
1592   NSButton *ok = [[NSButton alloc] initWithFrame:rect];
1593   [ok setTitle:@"OK"];
1594   [ok setBezelStyle:NSRoundedBezelStyle];
1595   [ok sizeToFit];
1596   rect = [bbox frame];
1597   rect.origin.x = rect.size.width - [ok frame].size.width;
1598   [ok setFrameOrigin:rect.origin];
1599
1600   rect = [ok frame];
1601   NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
1602   [cancel setTitle:@"Cancel"];
1603   [cancel setBezelStyle:NSRoundedBezelStyle];
1604   [cancel sizeToFit];
1605   rect.origin.x -= [cancel frame].size.width + 10;
1606   [cancel setFrameOrigin:rect.origin];
1607
1608   // Bind OK to RET and Cancel to ESC.
1609   [ok     setKeyEquivalent:@"\r"];
1610   [cancel setKeyEquivalent:@"\e"];
1611
1612   // The correct width for OK and Cancel buttons is 68 pixels
1613   // ("Human Interface Guidelines: Controls: Buttons: 
1614   // Push Button Specifications").
1615   //
1616   rect = [ok frame];
1617   rect.size.width = 68;
1618   [ok setFrame:rect];
1619
1620   rect = [cancel frame];
1621   rect.size.width = 68;
1622   [cancel setFrame:rect];
1623
1624   // It puts the buttons in the box or else it gets the hose again
1625   //
1626   [bbox addSubview:ok];
1627   [bbox addSubview:cancel];
1628   [bbox addSubview:reset];
1629   [bbox sizeToFit];
1630   
1631   // make a box to hold the button-box, and the preferences view
1632   //
1633   rect = [bbox frame];
1634   rect.origin.y += rect.size.height;
1635   NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
1636   [pbox setTitlePosition:NSNoTitle];
1637   [pbox setBorderType:NSNoBorder];
1638   [pbox addSubview:panel];
1639   [pbox addSubview:bbox];
1640   [pbox sizeToFit];
1641
1642   [reset  setAutoresizingMask:NSViewMaxXMargin];
1643   [cancel setAutoresizingMask:NSViewMinXMargin];
1644   [ok     setAutoresizingMask:NSViewMinXMargin];
1645   [bbox   setAutoresizingMask:NSViewWidthSizable];
1646   
1647   // grab the clicks
1648   //
1649   [ok     setTarget:window];
1650   [cancel setTarget:window];
1651   [reset  setTarget:window];
1652   [ok     setAction:@selector(okClicked:)];
1653   [cancel setAction:@selector(cancelClicked:)];
1654   [reset  setAction:@selector(resetClicked:)];
1655   
1656   return pbox;
1657 }
1658
1659
1660 /* Iterate over and process the children of the root node of the XML document.
1661  */
1662 static void
1663 traverse_tree (NSUserDefaultsController *prefs,
1664                NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node)
1665 {
1666   if (![[node name] isEqualToString:@"screensaver"]) {
1667     NSAssert (0, @"top level node is not <xscreensaver>");
1668   }
1669
1670   parse_xscreensaver_tag (node);
1671   
1672   NSRect rect;
1673   rect.origin.x = rect.origin.y = 0;
1674   rect.size.width = rect.size.height = 1;
1675
1676   NSView *panel = [[NSView alloc] initWithFrame:rect];
1677   
1678   traverse_children (prefs, opts, panel, node);
1679   fix_contentview_size (panel);
1680
1681   NSView *root = wrap_with_buttons (window, panel);
1682   [prefs setAppliesImmediately:NO];
1683
1684   [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1685
1686   rect = [window frameRectForContentRect:[root frame]];
1687   [window setFrame:rect display:NO];
1688   [window setMinSize:rect.size];
1689   
1690   [window setContentView:root];
1691 }
1692
1693
1694 /* When this object is instantiated, it parses the XML file and creates
1695    controls on itself that are hooked up to the appropriate preferences.
1696    The default size of the view is just big enough to hold them all.
1697  */
1698 - (id)initWithXMLFile: (NSString *) xml_file
1699               options: (const XrmOptionDescRec *) opts
1700            controller: (NSUserDefaultsController *) prefs
1701 {
1702   if (! (self = [super init]))
1703     return 0;
1704
1705   // instance variable
1706   userDefaultsController = prefs;
1707   [prefs retain];
1708
1709   NSURL *furl = [NSURL fileURLWithPath:xml_file];
1710
1711   if (!furl) {
1712     NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
1713     return nil;
1714   }
1715
1716   NSError *err = nil;
1717   NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] 
1718                             initWithContentsOfURL:furl
1719                             options:(NSXMLNodePreserveWhitespace |
1720                                      NSXMLNodePreserveCDATA)
1721                             error:&err];
1722 /* clean up?
1723     if (!xmlDoc) {
1724       xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl
1725                                       options:NSXMLDocumentTidyXML
1726                                       error:&err];
1727     }
1728 */
1729   if (!xmlDoc || err) {
1730     if (err)
1731       NSAssert2 (0, @"XML Error: %@: %@",
1732                  xml_file, [err localizedDescription]);
1733     return nil;
1734   }
1735
1736   traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
1737
1738   return self;
1739 }
1740
1741
1742 - (void) dealloc
1743 {
1744   [userDefaultsController release];
1745   [super dealloc];
1746 }
1747
1748 @end