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