http://www.jwz.org/xscreensaver/xscreensaver-5.09.tar.gz
[xscreensaver] / OSX / XScreenSaverConfigSheet.m
1 /* xscreensaver, Copyright (c) 2006-2009 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     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 either the min or max field contains a decimal point, then this
610   // option may have a floating point value; otherwise, it is constrained
611   // to be an integer.
612   //
613   NSCharacterSet *dot =
614     [NSCharacterSet characterSetWithCharactersInString:@"."];
615   BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
616                   [high rangeOfCharacterFromSet:dot].location != NSNotFound);
617
618   if ([type isEqualToString:@"slider"]) {
619
620     NSRect rect;
621     rect.origin.x = rect.origin.y = 0;    
622     rect.size.width = 150;
623     rect.size.height = 23;  // apparent min height for slider with ticks...
624     NSSlider *slider;
625     if (cvt)
626       slider = [[InvertedSlider alloc] initWithFrame:rect];
627     else
628       slider = [[NSSlider alloc] initWithFrame:rect];
629
630     [slider setMaxValue:[high doubleValue]];
631     [slider setMinValue:[low  doubleValue]];
632     
633     int range = [slider maxValue] - [slider minValue] + 1;
634     int range2 = range;
635     int max_ticks = 21;
636     while (range2 > max_ticks)
637       range2 /= 10;
638
639     // If we have elided ticks, leave it at the max number of ticks.
640     if (range != range2 && range2 < max_ticks)
641       range2 = max_ticks;
642
643     // If it's a float, always display the max number of ticks.
644     if (float_p && range2 < max_ticks)
645       range2 = max_ticks;
646
647     [slider setNumberOfTickMarks:range2];
648
649     [slider setAllowsTickMarkValuesOnly:
650               (range == range2 &&  // we are showing the actual number of ticks
651                !float_p)];         // and we want integer results
652
653     // #### Note: when the slider's range is large enough that we aren't
654     //      showing all possible ticks, the slider's value is not constrained
655     //      to be an integer, even though it should be...
656     //      Maybe we need to use a value converter or something?
657
658     if (label) {
659       NSTextField *lab = make_label (label);
660       place_child (parent, lab, NO);
661       [lab release];
662     }
663     
664     if (low_label) {
665       NSTextField *lab = make_label (low_label);
666       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
667       [lab setAlignment:1];  // right aligned
668       rect = [lab frame];
669       if (rect.size.width < LEFT_LABEL_WIDTH)
670         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
671       rect.size.height = [slider frame].size.height;
672       [lab setFrame:rect];
673       place_child (parent, lab, NO);
674       [lab release];
675      }
676     
677     place_child (parent, slider, (low_label ? YES : NO));
678     
679     if (! low_label) {
680       rect = [slider frame];
681       if (rect.origin.x < LEFT_LABEL_WIDTH)
682         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
683       [slider setFrame:rect];
684     }
685         
686     if (high_label) {
687       NSTextField *lab = make_label (high_label);
688       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
689       rect = [lab frame];
690       rect.size.height = [slider frame].size.height;
691       [lab setFrame:rect];
692       place_child (parent, lab, YES);
693       [lab release];
694      }
695
696     bind_switch_to_preferences (prefs, slider, arg, opts);
697     [slider release];
698     
699   } else if ([type isEqualToString:@"spinbutton"]) {
700
701     if (! label) {
702       NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
703       return;
704     }
705     NSAssert1 (!low_label,
706               @"low-label not allowed in spinbutton \"%@\"", [node name]);
707     NSAssert1 (!high_label,
708                @"high-label not allowed in spinbutton \"%@\"", [node name]);
709     NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
710                [node name]);
711     
712     NSRect rect;
713     rect.origin.x = rect.origin.y = 0;    
714     rect.size.width = rect.size.height = 10;
715     
716     NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
717     [txt setStringValue:@"0000.0"];
718     [txt sizeToFit];
719     [txt setStringValue:@""];
720     
721     if (label) {
722       NSTextField *lab = make_label (label);
723       //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
724       [lab setAlignment:1];  // right aligned
725       rect = [lab frame];
726       if (rect.size.width < LEFT_LABEL_WIDTH)
727         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
728       rect.size.height = [txt frame].size.height;
729       [lab setFrame:rect];
730       place_child (parent, lab, NO);
731       [lab release];
732      }
733     
734     place_child (parent, txt, (label ? YES : NO));
735     
736     if (! label) {
737       rect = [txt frame];
738       if (rect.origin.x < LEFT_LABEL_WIDTH)
739         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
740       [txt setFrame:rect];
741     }
742     
743     rect.size.width = rect.size.height = 10;
744     NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
745     [step sizeToFit];
746     place_child (parent, step, YES);
747     rect = [step frame];
748     rect.origin.x -= COLUMN_SPACING;  // this one goes close
749     rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
750     [step setFrame:rect];
751     
752     [step setMinValue:[low  doubleValue]];
753     [step setMaxValue:[high doubleValue]];
754     [step setAutorepeat:YES];
755     [step setValueWraps:NO];
756     
757     double range = [high doubleValue] - [low doubleValue];
758     if (range < 1.0)
759       [step setIncrement:range / 10.0];
760     else if (range >= 500)
761       [step setIncrement:range / 100.0];
762     else
763       [step setIncrement:1.0];
764
765     NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
766     [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
767     [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
768     [fmt setMinimum:[NSNumber numberWithDouble:[low  doubleValue]]];
769     [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
770     [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
771     [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
772
773     [fmt setGeneratesDecimalNumbers:float_p];
774     [[txt cell] setFormatter:fmt];
775
776
777     bind_switch_to_preferences (prefs, step, arg, opts);
778     bind_switch_to_preferences (prefs, txt,  arg, opts);
779     
780     [step release];
781     [txt release];
782     
783   } else {
784     NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
785   }
786 }
787
788
789 static void
790 set_menu_item_object (NSMenuItem *item, NSObject *obj)
791 {
792   /* If the object associated with this menu item looks like a boolean,
793      store an NSNumber instead of an NSString, since that's what
794      will be in the preferences (due to similar logic in PrefsReader).
795    */
796   if ([obj isKindOfClass:[NSString class]]) {
797     NSString *string = (NSString *) obj;
798     if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
799         NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
800       obj = [NSNumber numberWithBool:YES];
801     else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
802              NSOrderedSame == [string caseInsensitiveCompare:@"no"])
803       obj = [NSNumber numberWithBool:NO];
804     else
805       obj = string;
806   }
807
808   [item setRepresentedObject:obj];
809   //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
810 }
811
812
813 /* Creates the popup menu described by the given XML node (and its children).
814 */
815 static void
816 make_option_menu (NSUserDefaultsController *prefs,
817                   const XrmOptionDescRec *opts, 
818                   NSView *parent, NSXMLNode *node)
819 {
820   NSArray *children = [node children];
821   int i, count = [children count];
822
823   if (count <= 0) {
824     NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
825     return;
826   }
827
828   // get the "id" attribute off the <select> tag.
829   //
830   NSMutableDictionary *dict =
831     [NSMutableDictionary dictionaryWithObjectsAndKeys:
832       @"", @"id",
833       nil];
834   parse_attrs (dict, node);
835   
836   NSRect rect;
837   rect.origin.x = rect.origin.y = 0;
838   rect.size.width = 10;
839   rect.size.height = 10;
840   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
841                                                      pullsDown:NO];
842
843   NSMenuItem *def_item = nil;
844   float max_width = 0;
845   
846   NSString *menu_key = nil;   // the resource key used by items in this menu
847   
848   for (i = 0; i < count; i++) {
849     NSXMLNode *child = [children objectAtIndex:i];
850
851     if ([child kind] == NSXMLCommentKind)
852       continue;
853     if ([child kind] != NSXMLElementKind) {
854       NSAssert2 (0, @"weird XML node kind: %d: %@", [child kind], node);
855       continue;
856     }
857
858     // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
859     //
860     NSMutableDictionary *dict2 =
861       [NSMutableDictionary dictionaryWithObjectsAndKeys:
862         @"", @"id",
863         @"", @"_label",
864         @"", @"arg-set",
865         nil];
866     parse_attrs (dict2, child);
867     NSString *label   = [dict2 objectForKey:@"_label"];
868     NSString *arg_set = [dict2 objectForKey:@"arg-set"];
869     
870     if (!label) {
871       NSAssert1 (0, @"no _label in %@", [child name]);
872       return;
873     }
874
875     // create the menu item (and then get a pointer to it)
876     [popup addItemWithTitle:label];
877     NSMenuItem *item = [popup itemWithTitle:label];
878
879     if (arg_set) {
880       NSString *this_val = NULL;
881       NSString *this_key = switch_to_resource (arg_set, opts, &this_val);
882       NSAssert1 (this_val, @"this_val null for %@", arg_set);
883       if (menu_key && ![menu_key isEqualToString:this_key])
884         NSAssert3 (0,
885                    @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
886                    menu_key, this_key, this_val);
887       if (this_key)
888         menu_key = this_key;
889
890       /* If this menu has the cmd line "-mode foo" then set this item's
891          value to "foo" (the menu itself will be bound to e.g. "modeString")
892        */
893       set_menu_item_object (item, this_val);
894
895     } else {
896       // no arg-set -- only one menu item can be missing that.
897       NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
898       def_item = item;
899     }
900
901     /* make sure the menu button has room for the text of this item,
902        and remember the greatest width it has reached.
903      */
904     [popup setTitle:label];
905     [popup sizeToFit];
906     NSRect r = [popup frame];
907     if (r.size.width > max_width) max_width = r.size.width;
908   }
909   
910   if (!menu_key) {
911     NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
912     abort();
913   }
914
915   /* We've added all of the menu items.  If there was an item with no
916      command-line switch, then it's the item that represents the default
917      value.  Now we must bind to that item as well...  (We have to bind
918      this one late, because if it was the first item, then we didn't
919      yet know what resource was associated with this menu.)
920    */
921   if (def_item) {
922     NSDictionary *defs = [prefs initialValues];
923     NSObject *def_obj = [defs objectForKey:menu_key];
924
925     NSAssert2 (def_obj, 
926                @"no default value for resource \"%@\" in menu item \"%@\"",
927                menu_key, [def_item title]);
928
929     set_menu_item_object (def_item, def_obj);
930   }
931
932   /* Finish tweaking the menu button itself.
933    */
934   if (def_item)
935     [popup setTitle:[def_item title]];
936   NSRect r = [popup frame];
937   r.size.width = max_width;
938   [popup setFrame:r];
939   place_child (parent, popup, NO);
940
941   bind_resource_to_preferences (prefs, popup, menu_key, opts);
942   [popup release];
943 }
944
945
946 static NSString *unwrap (NSString *);
947 static void hreffify (NSText *);
948 static void boldify (NSText *);
949
950 /* Creates an uneditable, wrapping NSTextField to display the given
951    text enclosed by <description> ... </description> in the XML.
952  */
953 static void
954 make_desc_label (NSView *parent, NSXMLNode *node)
955 {
956   NSString *text = nil;
957   NSArray *children = [node children];
958   int i, count = [children count];
959
960   for (i = 0; i < count; i++) {
961     NSXMLNode *child = [children objectAtIndex:i];
962     NSString *s = [child objectValue];
963     if (text)
964       text = [text stringByAppendingString:s];
965     else
966       text = s;
967   }
968   
969   text = unwrap (text);
970   
971   NSRect rect = [parent frame];
972   rect.origin.x = rect.origin.y = 0;
973   rect.size.width = 200;
974   rect.size.height = 50;  // sized later
975   NSText *lab = [[NSText alloc] initWithFrame:rect];
976   [lab setEditable:NO];
977   [lab setDrawsBackground:NO];
978   [lab setHorizontallyResizable:YES];
979   [lab setVerticallyResizable:YES];
980   [lab setString:text];
981   hreffify (lab);
982   boldify (lab);
983   [lab sizeToFit];
984
985   place_child (parent, lab, NO);
986   [lab release];
987 }
988
989 static NSString *
990 unwrap (NSString *text)
991 {
992   // Unwrap lines: delete \n but do not delete \n\n.
993   //
994   NSArray *lines = [text componentsSeparatedByString:@"\n"];
995   int nlines = [lines count];
996   BOOL eolp = YES;
997   int i;
998
999   text = @"\n";      // start with one blank line
1000
1001   // skip trailing blank lines in file
1002   for (i = nlines-1; i > 0; i--) {
1003     NSString *s = (NSString *) [lines objectAtIndex:i];
1004     if ([s length] > 0)
1005       break;
1006     nlines--;
1007   }
1008
1009   // skip leading blank lines in file
1010   for (i = 0; i < nlines; i++) {
1011     NSString *s = (NSString *) [lines objectAtIndex:i];
1012     if ([s length] > 0)
1013       break;
1014   }
1015
1016   // unwrap
1017   Bool any = NO;
1018   for (; i < nlines; i++) {
1019     NSString *s = (NSString *) [lines objectAtIndex:i];
1020     if ([s length] == 0) {
1021       text = [text stringByAppendingString:@"\n\n"];
1022       eolp = YES;
1023     } else if ([s characterAtIndex:0] == ' ' ||
1024                [s hasPrefix:@"Copyright "] ||
1025                [s hasPrefix:@"http://"]) {
1026       // don't unwrap if the following line begins with whitespace,
1027       // or with the word "Copyright", or if it begins with a URL.
1028       if (any && !eolp)
1029         text = [text stringByAppendingString:@"\n"];
1030       text = [text stringByAppendingString:s];
1031       any = YES;
1032       eolp = NO;
1033     } else {
1034       if (!eolp)
1035         text = [text stringByAppendingString:@" "];
1036       text = [text stringByAppendingString:s];
1037       eolp = NO;
1038       any = YES;
1039     }
1040   }
1041
1042   return text;
1043 }
1044
1045
1046 static char *
1047 anchorize (const char *url)
1048 {
1049   const char *wiki = "http://en.wikipedia.org/wiki/";
1050   const char *math = "http://mathworld.wolfram.com/";
1051   if (!strncmp (wiki, url, strlen(wiki))) {
1052     char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1053     strcpy (anchor, "Wikipedia: \"");
1054     const char *in = url + strlen(wiki);
1055     char *out = anchor + strlen(anchor);
1056     while (*in) {
1057       if (*in == '_') {
1058         *out++ = ' ';
1059       } else if (*in == '#') {
1060         *out++ = ':';
1061         *out++ = ' ';
1062       } else if (*in == '%') {
1063         char hex[3];
1064         hex[0] = in[1];
1065         hex[1] = in[2];
1066         hex[2] = 0;
1067         int n = 0;
1068         sscanf (hex, "%x", &n);
1069         *out++ = (char) n;
1070         in += 2;
1071       } else {
1072         *out++ = *in;
1073       }
1074       in++;
1075     }
1076     *out++ = '"';
1077     *out = 0;
1078     return anchor;
1079
1080   } else if (!strncmp (math, url, strlen(math))) {
1081     char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1082     strcpy (anchor, "MathWorld: \"");
1083     const char *start = url + strlen(wiki);
1084     const char *in = start;
1085     char *out = anchor + strlen(anchor);
1086     while (*in) {
1087       if (*in == '_') {
1088         *out++ = ' ';
1089       } else if (in != start && *in >= 'A' && *in <= 'Z') {
1090         *out++ = ' ';
1091         *out++ = *in;
1092       } else if (!strncmp (in, ".htm", 4)) {
1093         break;
1094       } else {
1095         *out++ = *in;
1096       }
1097       in++;
1098     }
1099     *out++ = '"';
1100     *out = 0;
1101     return anchor;
1102
1103   } else {
1104     return strdup (url);
1105   }
1106 }
1107
1108
1109 /* Converts any http: URLs in the given text field to clickable links.
1110  */
1111 static void
1112 hreffify (NSText *nstext)
1113 {
1114   NSString *text = [nstext string];
1115   [nstext setRichText:YES];
1116
1117   int L = [text length];
1118   NSRange start;                // range is start-of-search to end-of-string
1119   start.location = 0;
1120   start.length = L;
1121   while (start.location < L) {
1122
1123     // Find the beginning of a URL...
1124     //
1125     NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1126     if (r2.location == NSNotFound)
1127       break;
1128
1129     // Next time around, start searching after this.
1130     start.location = r2.location + r2.length;
1131     start.length = L - start.location;
1132
1133     // Find the end of a URL (whitespace or EOF)...
1134     //
1135     NSRange r3 = [text rangeOfCharacterFromSet:
1136                          [NSCharacterSet whitespaceAndNewlineCharacterSet]
1137                        options:0 range:start];
1138     if (r3.location == NSNotFound)    // EOF
1139       r3.location = L, r3.length = 0;
1140
1141     // Next time around, start searching after this.
1142     start.location = r3.location;
1143     start.length = L - start.location;
1144
1145     // Set r2 to the start/length of this URL.
1146     r2.length = start.location - r2.location;
1147
1148     // Extract the URL.
1149     NSString *nsurl = [text substringWithRange:r2];
1150     const char *url = [nsurl UTF8String];
1151
1152     // If this is a Wikipedia URL, make the linked text be prettier.
1153     //
1154     char *anchor = anchorize(url);
1155
1156     // Construct the RTF corresponding to <A HREF="url">anchor</A>
1157     //
1158     const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1159     char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1160     sprintf (rtf, fmt, url, anchor);
1161     free (anchor);
1162     NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1163
1164     // Insert the RTF into the NSText.
1165     [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1166
1167     int L2 = [text length];  // might have changed
1168     start.location -= (L - L2);
1169     L = L2;
1170   }
1171 }
1172
1173 /* Makes the text up to the first comma be bold.
1174  */
1175 static void
1176 boldify (NSText *nstext)
1177 {
1178   NSString *text = [nstext string];
1179   NSRange r = [text rangeOfString:@"," options:0];
1180   r.length = r.location+1;
1181   r.location = 0;
1182
1183   NSFont *font = [nstext font];
1184   font = [NSFont boldSystemFontOfSize:[font pointSize]];
1185   [nstext setFont:font range:r];
1186 }
1187
1188
1189 static void layout_group (NSView *group, BOOL horiz_p);
1190
1191
1192 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
1193    wrapped in <hgroup> or <vgroup> in the XML.
1194  */
1195 static void
1196 make_group (NSUserDefaultsController *prefs,
1197             const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, 
1198             BOOL horiz_p)
1199 {
1200   NSRect rect;
1201   rect.size.width = rect.size.height = 1;
1202   rect.origin.x = rect.origin.y = 0;
1203   NSView *group = [[NSView alloc] initWithFrame:rect];
1204   traverse_children (prefs, opts, group, node);
1205
1206   layout_group (group, horiz_p);
1207
1208   rect.size.width = rect.size.height = 0;
1209   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1210   [box setTitlePosition:NSNoTitle];
1211   [box setBorderType:NSNoBorder];
1212   [box setContentViewMargins:rect.size];
1213   [box setContentView:group];
1214   [box sizeToFit];
1215
1216   place_child (parent, box, NO);
1217 }
1218
1219
1220 static void
1221 layout_group (NSView *group, BOOL horiz_p)
1222 {
1223   NSArray *kids = [group subviews];
1224   int nkids = [kids count];
1225   int i;
1226   double maxx = 0, miny = 0;
1227   for (i = 0; i < nkids; i++) {
1228     NSView *kid = [kids objectAtIndex:i];
1229     NSRect r = [kid frame];
1230     
1231     if (horiz_p) {
1232       maxx += r.size.width + COLUMN_SPACING;
1233       if (r.size.height > -miny) miny = -r.size.height;
1234     } else {
1235       if (r.size.width > maxx)  maxx = r.size.width;
1236       miny = r.origin.y - r.size.height;
1237     }
1238   }
1239   
1240   NSRect rect;
1241   rect.size.width = maxx;
1242   rect.size.height = -miny;
1243   [group setFrame:rect];
1244
1245   double x = 0;
1246   for (i = 0; i < nkids; i++) {
1247     NSView *kid = [kids objectAtIndex:i];
1248     NSRect r = [kid frame];
1249     if (horiz_p) {
1250       r.origin.y = rect.size.height - r.size.height;
1251       r.origin.x = x;
1252       x += r.size.width + COLUMN_SPACING;
1253     } else {
1254       r.origin.y -= miny;
1255     }
1256     [kid setFrame:r];
1257   }
1258 }
1259
1260
1261 static void
1262 make_text_controls (NSUserDefaultsController *prefs,
1263                     const XrmOptionDescRec *opts, 
1264                     NSView *parent, NSXMLNode *node)
1265 {
1266   /*
1267     Display Text:
1268      (x)  Computer name and time
1269      ( )  Text       [__________________________]
1270      ( )  Text file  [_________________] [Choose]
1271      ( )  URL        [__________________________]
1272
1273     textMode -text-mode date
1274     textMode -text-mode literal   textLiteral -text-literal %
1275     textMode -text-mode file      textFile    -text-file %
1276     textMode -text-mode url       textURL     -text-url %
1277    */
1278   NSRect rect;
1279   rect.size.width = rect.size.height = 1;
1280   rect.origin.x = rect.origin.y = 0;
1281   NSView *group = [[NSView alloc] initWithFrame:rect];
1282   NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1283
1284
1285   NSXMLElement *node2;
1286   NSView *control;
1287
1288   // This is how you link radio buttons together.
1289   //
1290   NSButtonCell *proto = [[NSButtonCell alloc] init];
1291   [proto setButtonType:NSRadioButton];
1292
1293   rect.origin.x = rect.origin.y = 0;
1294   rect.size.width = rect.size.height = 10;
1295   NSMatrix *matrix = [[NSMatrix alloc] 
1296                        initWithFrame:rect
1297                        mode:NSRadioModeMatrix
1298                        prototype:proto
1299                        numberOfRows:4
1300                        numberOfColumns:1];
1301   [matrix setAllowsEmptySelection:NO];
1302
1303   NSArrayController *cnames  = [[NSArrayController alloc] initWithContent:nil];
1304   [cnames addObject:@"Computer name and time"];
1305   [cnames addObject:@"Text"];
1306   [cnames addObject:@"File"];
1307   [cnames addObject:@"URL"];
1308   [matrix bind:@"content"
1309           toObject:cnames
1310           withKeyPath:@"arrangedObjects"
1311           options:nil];
1312   [cnames release];
1313
1314   bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts);
1315
1316   place_child (group, matrix, NO);
1317   place_child (group, rgroup, YES);
1318
1319   //  <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1320   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1321   [node2 setAttributesAsDictionary:
1322           [NSDictionary dictionaryWithObjectsAndKeys:
1323                         @"textLiteral",        @"id",
1324                         @"-text-literal %",    @"arg",
1325                         nil]];
1326   make_text_field (prefs, opts, rgroup, node2, YES);
1327   [node2 release];
1328
1329   rect = [last_child(rgroup) frame];
1330
1331 /* // trying to make the text fields be enabled only when the checkbox is on..
1332   control = last_child (rgroup);
1333   [control bind:@"enabled"
1334            toObject:[matrix cellAtRow:1 column:0]
1335            withKeyPath:@"value"
1336            options:nil];
1337 */
1338
1339
1340   //  <file id="textFile" _label="" arg-set="-text-file %"/>
1341   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1342   [node2 setAttributesAsDictionary:
1343           [NSDictionary dictionaryWithObjectsAndKeys:
1344                         @"textFile",           @"id",
1345                         @"-text-file %",       @"arg",
1346                         nil]];
1347   make_file_selector (prefs, opts, rgroup, node2, NO, YES);
1348   [node2 release];
1349
1350   rect = [last_child(rgroup) frame];
1351
1352   //  <string id="textURL" _label="" arg-set="text-url %"/>
1353   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1354   [node2 setAttributesAsDictionary:
1355           [NSDictionary dictionaryWithObjectsAndKeys:
1356                         @"textURL",            @"id",
1357                         @"-text-url %",        @"arg",
1358                         nil]];
1359   make_text_field (prefs, opts, rgroup, node2, YES);
1360   [node2 release];
1361
1362   rect = [last_child(rgroup) frame];
1363
1364   layout_group (rgroup, NO);
1365
1366   rect = [rgroup frame];
1367   rect.size.width += 35;    // WTF?  Why is rgroup too narrow?
1368   [rgroup setFrame:rect];
1369
1370
1371   // Set the height of the cells in the radio-box matrix to the height of
1372   // the (last of the) text fields.
1373   control = last_child (rgroup);
1374   rect = [control frame];
1375   rect.size.width = 30;  // width of the string "Text", plus a bit...
1376   rect.size.height += LINE_SPACING;
1377   [matrix setCellSize:rect.size];
1378   [matrix sizeToCells];
1379
1380   layout_group (group, YES);
1381   rect = [matrix frame];
1382   rect.origin.x += rect.size.width + COLUMN_SPACING;
1383   rect.origin.y -= [control frame].size.height - LINE_SPACING;
1384   [rgroup setFrameOrigin:rect.origin];
1385
1386   // now cheat on the size of the matrix: allow it to overlap (underlap)
1387   // the text fields.
1388   // 
1389   rect.size = [matrix cellSize];
1390   rect.size.width *= 10;
1391   [matrix setCellSize:rect.size];
1392   [matrix sizeToCells];
1393
1394   // Cheat on the position of the stuff on the right (the rgroup).
1395   // GAAAH, this code is such crap!
1396   rect = [rgroup frame];
1397   rect.origin.y -= 5;
1398   [rgroup setFrame:rect];
1399
1400
1401   rect.size.width = rect.size.height = 0;
1402   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1403   [box setTitlePosition:NSAtTop];
1404   [box setBorderType:NSBezelBorder];
1405   [box setTitle:@"Display Text"];
1406
1407   rect.size.width = rect.size.height = 12;
1408   [box setContentViewMargins:rect.size];
1409   [box setContentView:group];
1410   [box sizeToFit];
1411
1412   place_child (parent, box, NO);
1413 }
1414
1415
1416 static void
1417 make_image_controls (NSUserDefaultsController *prefs,
1418                      const XrmOptionDescRec *opts, 
1419                      NSView *parent, NSXMLNode *node)
1420 {
1421   /*
1422     [x]  Grab desktop images
1423     [ ]  Choose random image:
1424          [__________________________]  [Choose]
1425
1426    <boolean id="grabDesktopImages" _label="Grab desktop images"
1427        arg-unset="-no-grab-desktop"/>
1428    <boolean id="chooseRandomImages" _label="Grab desktop images"
1429        arg-unset="-choose-random-images"/>
1430    <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
1431    */
1432
1433   NSXMLElement *node2;
1434
1435   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1436   [node2 setAttributesAsDictionary:
1437           [NSDictionary dictionaryWithObjectsAndKeys:
1438                         @"grabDesktopImages",   @"id",
1439                         @"Grab desktop images", @"_label",
1440                         @"-no-grab-desktop",    @"arg-unset",
1441                         nil]];
1442   make_checkbox (prefs, opts, parent, node2);
1443   [node2 release];
1444
1445   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1446   [node2 setAttributesAsDictionary:
1447           [NSDictionary dictionaryWithObjectsAndKeys:
1448                         @"chooseRandomImages",    @"id",
1449                         @"Choose random images",  @"_label",
1450                         @"-choose-random-images", @"arg-set",
1451                         nil]];
1452   make_checkbox (prefs, opts, parent, node2);
1453   [node2 release];
1454
1455   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1456   [node2 setAttributesAsDictionary:
1457           [NSDictionary dictionaryWithObjectsAndKeys:
1458                         @"imageDirectory",     @"id",
1459                         @"Images directory:",  @"_label",
1460                         @"-image-directory %", @"arg",
1461                         nil]];
1462   make_file_selector (prefs, opts, parent, node2, YES, NO);
1463   [node2 release];
1464 }
1465
1466
1467
1468 /* Create some kind of control corresponding to the given XML node.
1469  */
1470 static void
1471 make_control (NSUserDefaultsController *prefs,
1472               const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
1473 {
1474   NSString *name = [node name];
1475
1476   if ([node kind] == NSXMLCommentKind)
1477     return;
1478   if ([node kind] != NSXMLElementKind) {
1479     NSAssert2 (0, @"weird XML node kind: %d: %@", [node kind], node);
1480     return;
1481   }
1482
1483   if ([name isEqualToString:@"hgroup"] ||
1484       [name isEqualToString:@"vgroup"]) {
1485
1486     BOOL horiz_p = [name isEqualToString:@"hgroup"];
1487     make_group (prefs, opts, parent, node, horiz_p);
1488
1489   } else if ([name isEqualToString:@"command"]) {
1490     // do nothing: this is the "-root" business
1491
1492   } else if ([name isEqualToString:@"boolean"]) {
1493     make_checkbox (prefs, opts, parent, node);
1494
1495   } else if ([name isEqualToString:@"string"]) {
1496     make_text_field (prefs, opts, parent, node, NO);
1497
1498   } else if ([name isEqualToString:@"file"]) {
1499     make_file_selector (prefs, opts, parent, node, NO, NO);
1500
1501   } else if ([name isEqualToString:@"number"]) {
1502     make_number_selector (prefs, opts, parent, node);
1503
1504   } else if ([name isEqualToString:@"select"]) {
1505     make_option_menu (prefs, opts, parent, node);
1506
1507   } else if ([name isEqualToString:@"_description"]) {
1508     make_desc_label (parent, node);
1509
1510   } else if ([name isEqualToString:@"xscreensaver-text"]) {
1511     make_text_controls (prefs, opts, parent, node);
1512
1513   } else if ([name isEqualToString:@"xscreensaver-image"]) {
1514     make_image_controls (prefs, opts, parent, node);
1515
1516   } else {
1517     NSAssert1 (0, @"unknown tag: %@", name);
1518   }
1519 }
1520
1521
1522 /* Iterate over and process the children of this XML node.
1523  */
1524 static void
1525 traverse_children (NSUserDefaultsController *prefs,
1526                    const XrmOptionDescRec *opts,
1527                    NSView *parent, NSXMLNode *node)
1528 {
1529   NSArray *children = [node children];
1530   int i, count = [children count];
1531   for (i = 0; i < count; i++) {
1532     NSXMLNode *child = [children objectAtIndex:i];
1533     make_control (prefs, opts, parent, child);
1534   }
1535 }
1536
1537 /* Handle the options on the top level <xscreensaver> tag.
1538  */
1539 static void
1540 parse_xscreensaver_tag (NSXMLNode *node)
1541 {
1542   NSMutableDictionary *dict =
1543   [NSMutableDictionary dictionaryWithObjectsAndKeys:
1544     @"", @"name",
1545     @"", @"_label",
1546     nil];
1547   parse_attrs (dict, node);
1548   NSString *name  = [dict objectForKey:@"name"];
1549   NSString *label = [dict objectForKey:@"_label"];
1550     
1551   if (!label) {
1552     NSAssert1 (0, @"no _label in %@", [node name]);
1553     return;
1554   }
1555   if (!name) {
1556     NSAssert1 (0, @"no name in \"%@\"", label);
1557     return;
1558   }
1559   
1560   // #### do any callers need the "name" field for anything?
1561 }
1562
1563
1564 /* Kludgey magic to make the window enclose the controls we created.
1565  */
1566 static void
1567 fix_contentview_size (NSView *parent)
1568 {
1569   NSRect f;
1570   NSArray *kids = [parent subviews];
1571   int nkids = [kids count];
1572   NSView *text;  // the NSText at the bottom of the window
1573   NSView *last;  // the last child before the NSText
1574   double maxx = 0, miny = 0;
1575   int i;
1576
1577   /* Find the size of the rectangle taken up by each of the children
1578      except the final "NSText" child.
1579   */
1580   for (i = 0; i < nkids; i++) {
1581     NSView *kid = [kids objectAtIndex:i];
1582     if ([kid isKindOfClass:[NSText class]]) {
1583       text = kid;
1584       continue;
1585     }
1586     f = [kid frame];
1587     if (f.origin.x + f.size.width > maxx)  maxx = f.origin.x + f.size.width;
1588     if (f.origin.y - f.size.height < miny) miny = f.origin.y;
1589     last = kid;
1590 //    NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1591 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1592 //          f.origin.y + f.size.height, [kid class]);
1593   }
1594   
1595   if (maxx < 400) maxx = 400;   // leave room for the NSText paragraph...
1596   
1597   /* Now that we know the width of the window, set the width of the NSText to
1598      that, so that it can decide what its height needs to be.
1599    */
1600   f = [text frame];
1601 //  NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1602 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1603 //        f.origin.y + f.size.height, [text class]);
1604   
1605   // set the NSText's width (this changes its height).
1606   f.size.width = maxx - LEFT_MARGIN;
1607   [text setFrame:f];
1608   
1609   // position the NSText below the last child (this gives us a new miny).
1610   f = [text frame];
1611   f.origin.y = miny - f.size.height - LINE_SPACING;
1612   miny = f.origin.y - LINE_SPACING;
1613   [text setFrame:f];
1614   
1615   // Lock the width of the field and unlock the height, and let it resize
1616   // once more, to compute the proper height of the text for that width.
1617   //
1618   [(NSText *) text setHorizontallyResizable:NO];
1619   [(NSText *) text setVerticallyResizable:YES];
1620   [(NSText *) text sizeToFit];
1621
1622   // Now lock the height too: no more resizing this text field.
1623   //
1624   [(NSText *) text setVerticallyResizable:NO];
1625
1626   // Now reposition the top edge of the text field to be back where it
1627   // was before we changed the height.
1628   //
1629   float oh = f.size.height;
1630   f = [text frame];
1631   float dh = f.size.height - oh;
1632   f.origin.y += dh;
1633
1634   // #### This is needed in OSX 10.5, but is wrong in OSX 10.6.  WTF??
1635   //      If we do this in 10.6, the text field moves down, off the window.
1636   //      So instead we repair it at the end, at the "WTF2" comment.
1637   [text setFrame:f];
1638
1639   // Also adjust the parent height by the change in height of the text field.
1640   miny -= dh;
1641
1642 //  NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1643 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1644 //        f.origin.y + f.size.height, [text class]);
1645   
1646   
1647   /* Set the contentView to the size of the children.
1648    */
1649   f = [parent frame];
1650   float yoff = f.size.height;
1651   f.size.width = maxx + LEFT_MARGIN;
1652   f.size.height = -(miny - LEFT_MARGIN*2);
1653   yoff = f.size.height - yoff;
1654   [parent setFrame:f];
1655
1656 //  NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", 
1657 //        f.size.width, f.size.height, f.origin.x, f.origin.y);
1658
1659   /* Now move all of the kids up into the window.
1660    */
1661   f = [parent frame];
1662   float shift = f.size.height;
1663 //  NSLog(@"shift: %3.0f", shift);
1664   for (i = 0; i < nkids; i++) {
1665     NSView *kid = [kids objectAtIndex:i];
1666     f = [kid frame];
1667     f.origin.y += shift;
1668     [kid setFrame:f];
1669 //    NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1670 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1671 //          f.origin.y + f.size.height, [kid class]);
1672   }
1673   
1674 /*
1675 Bad:
1676  parent: 420 x 541 @   0   0
1677  text:   380 x 100 @  20  22  miny=-501
1678
1679 Good:
1680  parent: 420 x 541 @   0   0
1681  text:   380 x 100 @  20  50  miny=-501
1682 */
1683
1684   // #### WTF2: See "WTF" above.  If the text field is off the screen,
1685   //      move it up.  We need this on 10.6 but not on 10.5.  Auugh.
1686   //
1687   f = [text frame];
1688   if (f.origin.y < 50) {    // magic numbers, yay
1689     f.origin.y = 50;
1690     [text setFrame:f];
1691   }
1692
1693   /* Set the kids to track the top left corner of the window when resized.
1694      Set the NSText to track the bottom right corner as well.
1695    */
1696   for (i = 0; i < nkids; i++) {
1697     NSView *kid = [kids objectAtIndex:i];
1698     unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
1699     if ([kid isKindOfClass:[NSText class]])
1700       mask |= NSViewWidthSizable|NSViewHeightSizable;
1701     [kid setAutoresizingMask:mask];
1702   }
1703 }
1704
1705
1706 - (void) okClicked:(NSObject *)arg
1707 {
1708   [userDefaultsController commitEditing];
1709   [userDefaultsController save:self];
1710   [NSApp endSheet:self returnCode:NSOKButton];
1711   [self close];
1712 }
1713
1714 - (void) cancelClicked:(NSObject *)arg
1715 {
1716   [userDefaultsController revert:self];
1717   [NSApp endSheet:self returnCode:NSCancelButton];
1718   [self close];
1719 }
1720
1721 - (void) resetClicked:(NSObject *)arg
1722 {
1723   [userDefaultsController revertToInitialValues:self];
1724 }
1725
1726
1727 static NSView *
1728 wrap_with_buttons (NSWindow *window, NSView *panel)
1729 {
1730   NSRect rect;
1731   
1732   // Make a box to hold the buttons at the bottom of the window.
1733   //
1734   rect = [panel frame];
1735   rect.origin.x = rect.origin.y = 0;
1736   rect.size.height = 10;
1737   NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
1738   [bbox setTitlePosition:NSNoTitle];  
1739   [bbox setBorderType:NSNoBorder];
1740   
1741   // Make some buttons: Default, Cancel, OK
1742   //
1743   rect.origin.x = rect.origin.y = 0;
1744   rect.size.width = rect.size.height = 10;
1745   NSButton *reset = [[NSButton alloc] initWithFrame:rect];
1746   [reset setTitle:@"Reset to Defaults"];
1747   [reset setBezelStyle:NSRoundedBezelStyle];
1748   [reset sizeToFit];
1749
1750   rect = [reset frame];
1751   NSButton *ok = [[NSButton alloc] initWithFrame:rect];
1752   [ok setTitle:@"OK"];
1753   [ok setBezelStyle:NSRoundedBezelStyle];
1754   [ok sizeToFit];
1755   rect = [bbox frame];
1756   rect.origin.x = rect.size.width - [ok frame].size.width;
1757   [ok setFrameOrigin:rect.origin];
1758
1759   rect = [ok frame];
1760   NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
1761   [cancel setTitle:@"Cancel"];
1762   [cancel setBezelStyle:NSRoundedBezelStyle];
1763   [cancel sizeToFit];
1764   rect.origin.x -= [cancel frame].size.width + 10;
1765   [cancel setFrameOrigin:rect.origin];
1766
1767   // Bind OK to RET and Cancel to ESC.
1768   [ok     setKeyEquivalent:@"\r"];
1769   [cancel setKeyEquivalent:@"\e"];
1770
1771   // The correct width for OK and Cancel buttons is 68 pixels
1772   // ("Human Interface Guidelines: Controls: Buttons: 
1773   // Push Button Specifications").
1774   //
1775   rect = [ok frame];
1776   rect.size.width = 68;
1777   [ok setFrame:rect];
1778
1779   rect = [cancel frame];
1780   rect.size.width = 68;
1781   [cancel setFrame:rect];
1782
1783   // It puts the buttons in the box or else it gets the hose again
1784   //
1785   [bbox addSubview:ok];
1786   [bbox addSubview:cancel];
1787   [bbox addSubview:reset];
1788   [bbox sizeToFit];
1789   
1790   // make a box to hold the button-box, and the preferences view
1791   //
1792   rect = [bbox frame];
1793   rect.origin.y += rect.size.height;
1794   NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
1795   [pbox setTitlePosition:NSNoTitle];
1796   [pbox setBorderType:NSBezelBorder];
1797
1798   // Enforce a max height on the dialog, so that it's obvious to me
1799   // (on a big screen) when the dialog will fall off the bottom of
1800   // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
1801   {
1802     NSRect f = [panel frame];
1803     int screen_height = (768    // shortest "modern" Mac display
1804                          - 22   // menu bar
1805                          - 56   // System Preferences toolbar
1806                          - 140  // default magnified bottom dock icon
1807                          );
1808     if (f.size.height > screen_height) {
1809       NSLog(@"%@ height was %.0f; clipping to %d", 
1810           [panel class], f.size.height, screen_height);
1811       f.size.height = screen_height;
1812       [panel setFrame:f];
1813     }
1814   }
1815
1816   [pbox addSubview:panel];
1817   [pbox addSubview:bbox];
1818   [pbox sizeToFit];
1819
1820   [reset  setAutoresizingMask:NSViewMaxXMargin];
1821   [cancel setAutoresizingMask:NSViewMinXMargin];
1822   [ok     setAutoresizingMask:NSViewMinXMargin];
1823   [bbox   setAutoresizingMask:NSViewWidthSizable];
1824   
1825   // grab the clicks
1826   //
1827   [ok     setTarget:window];
1828   [cancel setTarget:window];
1829   [reset  setTarget:window];
1830   [ok     setAction:@selector(okClicked:)];
1831   [cancel setAction:@selector(cancelClicked:)];
1832   [reset  setAction:@selector(resetClicked:)];
1833   
1834   return pbox;
1835 }
1836
1837
1838 /* Iterate over and process the children of the root node of the XML document.
1839  */
1840 static void
1841 traverse_tree (NSUserDefaultsController *prefs,
1842                NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node)
1843 {
1844   if (![[node name] isEqualToString:@"screensaver"]) {
1845     NSAssert (0, @"top level node is not <xscreensaver>");
1846   }
1847
1848   parse_xscreensaver_tag (node);
1849   
1850   NSRect rect;
1851   rect.origin.x = rect.origin.y = 0;
1852   rect.size.width = rect.size.height = 1;
1853
1854   NSView *panel = [[NSView alloc] initWithFrame:rect];
1855   
1856   traverse_children (prefs, opts, panel, node);
1857   fix_contentview_size (panel);
1858
1859   NSView *root = wrap_with_buttons (window, panel);
1860   [prefs setAppliesImmediately:NO];
1861
1862   [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1863
1864   rect = [window frameRectForContentRect:[root frame]];
1865   [window setFrame:rect display:NO];
1866   [window setMinSize:rect.size];
1867   
1868   [window setContentView:root];
1869 }
1870
1871
1872 /* When this object is instantiated, it parses the XML file and creates
1873    controls on itself that are hooked up to the appropriate preferences.
1874    The default size of the view is just big enough to hold them all.
1875  */
1876 - (id)initWithXMLFile: (NSString *) xml_file
1877               options: (const XrmOptionDescRec *) opts
1878            controller: (NSUserDefaultsController *) prefs
1879 {
1880   if (! (self = [super init]))
1881     return 0;
1882
1883   // instance variable
1884   userDefaultsController = prefs;
1885   [prefs retain];
1886
1887   NSURL *furl = [NSURL fileURLWithPath:xml_file];
1888
1889   if (!furl) {
1890     NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
1891     return nil;
1892   }
1893
1894   NSError *err = nil;
1895   NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] 
1896                             initWithContentsOfURL:furl
1897                             options:(NSXMLNodePreserveWhitespace |
1898                                      NSXMLNodePreserveCDATA)
1899                             error:&err];
1900 /* clean up?
1901     if (!xmlDoc) {
1902       xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl
1903                                       options:NSXMLDocumentTidyXML
1904                                       error:&err];
1905     }
1906 */
1907   if (!xmlDoc || err) {
1908     if (err)
1909       NSAssert2 (0, @"XML Error: %@: %@",
1910                  xml_file, [err localizedDescription]);
1911     return nil;
1912   }
1913
1914   traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
1915
1916   return self;
1917 }
1918
1919
1920 - (void) dealloc
1921 {
1922   [userDefaultsController release];
1923   [super dealloc];
1924 }
1925
1926 @end