06804c70429b915d84eb34dcbe30ad90fde0889c
[xscreensaver] / OSX / XScreenSaverConfigSheet.m
1 /* xscreensaver, Copyright (c) 2006-2008 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* XScreenSaver uses XML files to describe the user interface for configuring
13    the various screen savers.  These files live in .../hacks/config/ and
14    say relatively high level things like: "there should be a checkbox
15    labelled "Leave Trails", and when it is checked, add the option '-trails'
16    to the command line when launching the program."
17
18    This code reads that XML and constructs a Cocoa interface from it.
19    The Cocoa controls are hooked up to NSUserDefaultsController to save
20    those settings into the MacOS preferences system.  The Cocoa preferences
21    names are the same as the resource names specified in the screenhack's
22    'options' array (we use that array to map the command line switches
23    specified in the XML to the resource names to use).
24  */
25
26 #import "XScreenSaverConfigSheet.h"
27
28 #import "jwxyz.h"
29 #import "InvertedSlider.h"
30 #import <Foundation/NSXMLDocument.h>
31
32 @implementation XScreenSaverConfigSheet
33
34 #define LEFT_MARGIN       20   // left edge of window
35 #define COLUMN_SPACING    10   // gap between e.g. labels and text fields
36 #define LEFT_LABEL_WIDTH  70   // width of all left labels
37 #define LINE_SPACING      10   // leading between each line
38
39 // redefine these since they don't work when not inside an ObjC method
40 #undef NSAssert
41 #undef NSAssert1
42 #undef NSAssert2
43 #undef NSAssert3
44 #define NSAssert(CC,S)        do { if (!(CC)) { NSLog(S);       }} while(0)
45 #define NSAssert1(CC,S,A)     do { if (!(CC)) { NSLog(S,A);     }} while(0)
46 #define NSAssert2(CC,S,A,B)   do { if (!(CC)) { NSLog(S,A,B);   }} while(0)
47 #define NSAssert3(CC,S,A,B,C) do { if (!(CC)) { NSLog(S,A,B,C); }} while(0)
48
49
50 /* Given a command-line option, returns the corresponding resource name.
51    Any arguments in the switch string are ignored (e.g., "-foo x").
52  */
53 static NSString *
54 switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts,
55                     NSString **val_ret)
56 {
57   char buf[255];
58   char *tail = 0;
59   NSAssert(cmdline_switch, @"cmdline switch is null");
60   if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
61                           encoding:NSUTF8StringEncoding]) {
62     NSAssert1(0, @"unable to convert %@", cmdline_switch);
63     abort();
64   }
65   char *s = strpbrk(buf, " \t\r\n");
66   if (s && *s) {
67     *s = 0;
68     tail = s+1;
69     while (*tail && (*tail == ' ' || *tail == '\t'))
70       tail++;
71   }
72   
73   while (opts[0].option) {
74     if (!strcmp (opts[0].option, buf)) {
75       const char *ret = 0;
76
77       if (opts[0].argKind == XrmoptionNoArg) {
78         if (tail && *tail)
79           NSAssert1 (0, @"expected no args to switch: \"%@\"",
80                      cmdline_switch);
81         ret = opts[0].value;
82       } else {
83         if (!tail || !*tail)
84           NSAssert1 (0, @"expected args to switch: \"%@\"",
85                      cmdline_switch);
86         ret = tail;
87       }
88
89       if (val_ret)
90         *val_ret = (ret
91                     ? [NSString stringWithCString:ret
92                                          encoding:NSUTF8StringEncoding]
93                     : 0);
94       
95       const char *res = opts[0].specifier;
96       while (*res && (*res == '.' || *res == '*'))
97         res++;
98       return [NSString stringWithCString:res
99                                 encoding:NSUTF8StringEncoding];
100     }
101     opts++;
102   }
103   
104   NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
105   abort();
106 }
107
108
109 /* Connects a control (checkbox, etc) to the corresponding preferences key.
110  */
111 static void
112 bind_resource_to_preferences (NSUserDefaultsController *prefs,
113                               NSObject *control, 
114                               NSString *pref_key,
115                               const XrmOptionDescRec *opts)
116 {
117   NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
118                       ? @"selectedObject"
119                       : ([control isKindOfClass:[NSMatrix class]]
120                          ? @"selectedIndex"
121                          : @"value"));
122   [control bind:bindto
123        toObject:prefs
124     withKeyPath:[@"values." stringByAppendingString: pref_key]
125         options:nil];
126
127 # if 0 // ####
128   NSObject *def = [[prefs defaults] objectForKey:pref_key];
129   NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
130   s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
131   s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
132   s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
133   NSLog (@"%@ %@/%@", s, [def class], [control class]);
134 # endif
135 }
136
137 static void
138 bind_switch_to_preferences (NSUserDefaultsController *prefs,
139                             NSObject *control, 
140                             NSString *cmdline_switch,
141                             const XrmOptionDescRec *opts)
142 {
143   NSString *pref_key = switch_to_resource (cmdline_switch, opts, 0);
144   bind_resource_to_preferences (prefs, control, pref_key, opts);
145 }
146
147
148 /* Parse the attributes of an XML tag into a dictionary.
149    For input, the dictionary should have as attributes the keys, each
150    with @"" as their value.
151    On output, the dictionary will set the keys to the values specified,
152    and keys that were not specified will not be present in the dictionary.
153    Warnings are printed if there are duplicate or unknown attributes.
154  */
155 static void
156 parse_attrs (NSMutableDictionary *dict, NSXMLNode *node)
157 {
158   NSArray *attrs = [(NSXMLElement *) node attributes];
159   int n = [attrs count];
160   int i;
161   
162   // For each key in the dictionary, fill in the dict with the corresponding
163   // value.  The value @"" is assumed to mean "un-set".  Issue a warning if
164   // an attribute is specified twice.
165   //
166   for (i = 0; i < n; i++) {
167     NSXMLNode *attr = [attrs objectAtIndex:i];
168     NSString *key = [attr name];
169     NSString *val = [attr objectValue];
170     NSString *old = [dict objectForKey:key];
171     
172     if (! old) {
173       NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
174     } else if ([old length] != 0) {
175       NSAssert2 (0, @"duplicate %@: \"%@\", \"%@\"", old, val);
176     } else {
177       [dict setValue:val forKey:key];
178     }
179   }
180   
181   // Remove from the dictionary any keys whose value is still @"", 
182   // meaning there was no such attribute specified.
183   //
184   NSArray *keys = [dict allKeys];
185   n = [keys count];
186   for (i = 0; i < n; i++) {
187     NSString *key = [keys objectAtIndex:i];
188     NSString *val = [dict objectForKey:key];
189     if ([val length] == 0)
190       [dict removeObjectForKey:key];
191   }
192 }
193
194
195 /* Creates a label: an un-editable NSTextField displaying the given text.
196  */
197 static NSTextField *
198 make_label (NSString *text)
199 {
200   NSRect rect;
201   rect.origin.x = rect.origin.y = 0;
202   rect.size.width = rect.size.height = 10;
203   NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
204   [lab setSelectable:NO];
205   [lab setEditable:NO];
206   [lab setBezeled:NO];
207   [lab setDrawsBackground:NO];
208   [lab setStringValue:text];
209   [lab sizeToFit];
210   return lab;
211 }
212
213
214 static NSView *
215 last_child (NSView *parent)
216 {
217   NSArray *kids = [parent subviews];
218   int nkids = [kids count];
219   if (nkids == 0)
220     return 0;
221   else
222     return [kids objectAtIndex:nkids-1];
223 }
224
225
226 /* Add the child as a subview of the parent, positioning it immediately
227    below or to the right of the previously-added child of that view.
228  */
229 static void
230 place_child (NSView *parent, NSView *child, BOOL right_p)
231 {
232   NSRect rect = [child frame];
233   NSView *last = last_child (parent);
234   if (!last) {
235     rect.origin.x = LEFT_MARGIN;
236     rect.origin.y = [parent frame].size.height - rect.size.height 
237       - LINE_SPACING;
238   } else if (right_p) {
239     rect = [last frame];
240     rect.origin.x += rect.size.width + COLUMN_SPACING;
241   } else {
242     rect = [last frame];
243     rect.origin.x = LEFT_MARGIN;
244     rect.origin.y -= [child frame].size.height + LINE_SPACING;
245   }
246   [child setFrameOrigin:rect.origin];
247   [parent addSubview:child];
248 }
249
250
251 static void traverse_children (NSUserDefaultsController *,
252                                const XrmOptionDescRec *, 
253                                NSView *, NSXMLNode *);
254
255
256 /* Creates the checkbox (NSButton) described by the given XML node.
257  */
258 static void
259 make_checkbox (NSUserDefaultsController *prefs,
260                const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
261 {
262   NSMutableDictionary *dict =
263     [NSMutableDictionary dictionaryWithObjectsAndKeys:
264       @"", @"id",
265       @"", @"_label",
266       @"", @"arg-set",
267       @"", @"arg-unset",
268       nil];
269   parse_attrs (dict, node);
270   NSString *label     = [dict objectForKey:@"_label"];
271   NSString *arg_set   = [dict objectForKey:@"arg-set"];
272   NSString *arg_unset = [dict objectForKey:@"arg-unset"];
273   
274   if (!label) {
275     NSAssert1 (0, @"no _label in %@", [node name]);
276     return;
277   }
278   if (!arg_set && !arg_unset) {
279     NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"", 
280                label);
281   }
282   if (arg_set && arg_unset) {
283     NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"", 
284                label);
285   }
286   
287   // sanity-check the choice of argument names.
288   //
289   if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
290                   [arg_set hasPrefix:@"--no-"]))
291     NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
292            label, arg_set);
293   if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
294                     ![arg_unset hasPrefix:@"--no-"]))
295     NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
296           label, arg_unset);
297     
298   NSRect rect;
299   rect.origin.x = rect.origin.y = 0;
300   rect.size.width = rect.size.height = 10;
301
302   NSButton *button = [[NSButton alloc] initWithFrame:rect];
303   [button setButtonType:([[node name] isEqualToString:@"radio"]
304                          ? NSRadioButton
305                          : NSSwitchButton)];
306   [button setTitle:label];
307   [button sizeToFit];
308   place_child (parent, button, NO);
309   
310   bind_switch_to_preferences (prefs, button,
311                               (arg_set ? arg_set : arg_unset),
312                               opts);
313   [button release];
314 }
315
316
317 /* Creates the NSTextField described by the given XML node.
318 */
319 static void
320 make_text_field (NSUserDefaultsController *prefs,
321                  const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node,
322                  BOOL no_label_p)
323 {
324   NSMutableDictionary *dict =
325   [NSMutableDictionary dictionaryWithObjectsAndKeys:
326     @"", @"id",
327     @"", @"_label",
328     @"", @"arg",
329     nil];
330   parse_attrs (dict, node);
331   NSString *label = [dict objectForKey:@"_label"];
332   NSString *arg   = [dict objectForKey:@"arg"];
333
334   if (!label && !no_label_p) {
335     NSAssert1 (0, @"no _label in %@", [node name]);
336     return;
337   }
338
339   NSAssert1 (arg, @"no arg in %@", label);
340
341   NSRect rect;
342   rect.origin.x = rect.origin.y = 0;    
343   rect.size.width = rect.size.height = 10;
344   
345   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
346
347   // make the default size be around 30 columns; a typical value for
348   // these text fields is "xscreensaver-text --cols 40".
349   //
350   [txt setStringValue:@"123456789 123456789 123456789 "];
351   [txt sizeToFit];
352   [[txt cell] setWraps:NO];
353   [[txt cell] setScrollable:YES];
354   [txt setStringValue:@""];
355   
356   if (label) {
357     NSTextField *lab = make_label (label);
358     place_child (parent, lab, NO);
359     [lab release];
360   }
361
362   place_child (parent, txt, (label ? YES : NO));
363
364   bind_switch_to_preferences (prefs, txt, arg, opts);
365   [txt release];
366 }
367
368
369 /* Creates the NSTextField described by the given XML node,
370    and hooks it up to a Choose button and a file selector widget.
371 */
372 static void
373 make_file_selector (NSUserDefaultsController *prefs,
374                     const XrmOptionDescRec *opts, 
375                     NSView *parent, NSXMLNode *node,
376                     BOOL dirs_only_p,
377                     BOOL no_label_p)
378 {
379   NSMutableDictionary *dict =
380   [NSMutableDictionary dictionaryWithObjectsAndKeys:
381     @"", @"id",
382     @"", @"_label",
383     @"", @"arg",
384     nil];
385   parse_attrs (dict, node);
386   NSString *label = [dict objectForKey:@"_label"];
387   NSString *arg   = [dict objectForKey:@"arg"];
388
389   if (!label && !no_label_p) {
390     NSAssert1 (0, @"no _label in %@", [node name]);
391     return;
392   }
393
394   NSAssert1 (arg, @"no arg in %@", label);
395
396   NSRect rect;
397   rect.origin.x = rect.origin.y = 0;    
398   rect.size.width = rect.size.height = 10;
399   
400   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
401
402   // make the default size be around 20 columns.
403   //
404   [txt setStringValue:@"123456789 123456789 "];
405   [txt sizeToFit];
406   [txt setSelectable:YES];
407   [txt setEditable:NO];
408   [txt setBezeled:NO];
409   [txt setDrawsBackground:NO];
410   [[txt cell] setWraps:NO];
411   [[txt cell] setScrollable:YES];
412   [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
413   [txt setStringValue:@""];
414
415   NSTextField *lab = 0;
416   if (label) {
417     lab = make_label (label);
418     place_child (parent, lab, NO);
419     [lab release];
420   }
421
422   place_child (parent, txt, (label ? YES : NO));
423
424   bind_switch_to_preferences (prefs, txt, arg, opts);
425   [txt release];
426
427   // Make the text field be the same height as the label.
428   if (lab) {
429     rect = [txt frame];
430     rect.size.height = [lab frame].size.height;
431     [txt setFrame:rect];
432   }
433
434   // Now put a "Choose" button next to it.
435   //
436   rect.origin.x = rect.origin.y = 0;    
437   rect.size.width = rect.size.height = 10;
438   NSButton *choose = [[NSButton alloc] initWithFrame:rect];
439   [choose setTitle:@"Choose..."];
440   [choose setBezelStyle:NSRoundedBezelStyle];
441   [choose sizeToFit];
442
443   place_child (parent, choose, YES);
444
445   // center the Choose button around the midpoint of the text field.
446   rect = [choose frame];
447   rect.origin.y = ([txt frame].origin.y + 
448                    (([txt frame].size.height - rect.size.height) / 2));
449   [choose setFrameOrigin:rect.origin];
450
451   [choose setTarget:[parent window]];
452   if (dirs_only_p)
453     [choose setAction:@selector(chooseClickedDirs:)];
454   else
455     [choose setAction:@selector(chooseClicked:)];
456
457   [choose release];
458 }
459
460
461 /* Runs a modal file selector and sets the text field's value to the
462    selected file or directory.
463  */
464 static void
465 do_file_selector (NSTextField *txt, BOOL dirs_p)
466 {
467   NSOpenPanel *panel = [NSOpenPanel openPanel];
468   [panel setAllowsMultipleSelection:NO];
469   [panel setCanChooseFiles:!dirs_p];
470   [panel setCanChooseDirectories:dirs_p];
471
472   NSString *file = [txt stringValue];
473   if ([file length] <= 0) {
474     file = NSHomeDirectory();
475     if (dirs_p)
476       file = [file stringByAppendingPathComponent:@"Pictures"];
477   }
478
479 //  NSString *dir = [file stringByDeletingLastPathComponent];
480
481   int result = [panel runModalForDirectory:file //dir
482                                       file:nil //[file lastPathComponent]
483                                      types:nil];
484   if (result == NSOKButton) {
485     NSArray *files = [panel filenames];
486     NSString *file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
487     file = [file stringByAbbreviatingWithTildeInPath];
488     [txt setStringValue:file];
489
490     // Fuck me!  Just setting the value of the NSTextField does not cause
491     // that to end up in the preferences!
492     //
493     NSDictionary *dict = [txt infoForBinding:@"value"];
494     NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
495     NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
496     if ([path hasPrefix:@"values."])  // WTF.
497       path = [path substringFromIndex:7];
498     [[prefs values] setValue:file forKey:path];
499
500 #if 0
501     // make sure the end of the string is visible.
502     NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
503     NSRange range;
504     range.location = [file length]-3;
505     range.length = 1;
506     if (! [[txt window] makeFirstResponder:[txt window]])
507       [[txt window] endEditingFor:nil];
508 //    [[txt window] makeFirstResponder:nil];
509     [fe setSelectedRange:range];
510 //    [tv scrollRangeToVisible:range];
511 //    [txt setNeedsDisplay:YES];
512 //    [[txt cell] setNeedsDisplay:YES];
513 //    [txt selectAll:txt];
514 #endif
515   }
516 }
517
518 /* Returns the NSTextField that is to the left of or above the NSButton.
519  */
520 static NSTextField *
521 find_text_field_of_button (NSButton *button)
522 {
523   NSView *parent = [button superview];
524   NSArray *kids = [parent subviews];
525   int nkids = [kids count];
526   int i;
527   NSTextField *f = 0;
528   for (i = 0; i < nkids; i++) {
529     NSObject *kid = [kids objectAtIndex:i];
530     if ([kid isKindOfClass:[NSTextField class]]) {
531       f = (NSTextField *) kid;
532     } else if (kid == button) {
533       if (! f) abort();
534       return f;
535     }
536   }
537   abort();
538 }
539
540
541 - (void) chooseClicked:(NSObject *)arg
542 {
543   NSButton *choose = (NSButton *) arg;
544   NSTextField *txt = find_text_field_of_button (choose);
545   do_file_selector (txt, NO);
546 }
547
548 - (void) chooseClickedDirs:(NSObject *)arg
549 {
550   NSButton *choose = (NSButton *) arg;
551   NSTextField *txt = find_text_field_of_button (choose);
552   do_file_selector (txt, YES);
553 }
554
555
556 /* Creates the number selection control described by the given XML node.
557    If "type=slider", it's an NSSlider.
558    If "type=spinbutton", it's a text field with up/down arrows next to it.
559 */
560 static void
561 make_number_selector (NSUserDefaultsController *prefs,
562                       const XrmOptionDescRec *opts, 
563                       NSView *parent, NSXMLNode *node)
564 {
565   NSMutableDictionary *dict =
566   [NSMutableDictionary dictionaryWithObjectsAndKeys:
567     @"", @"id",
568     @"", @"_label",
569     @"", @"_low-label",
570     @"", @"_high-label",
571     @"", @"type",
572     @"", @"arg",
573     @"", @"low",
574     @"", @"high",
575     @"", @"default",
576     @"", @"convert",
577     nil];
578   parse_attrs (dict, node);
579   NSString *label      = [dict objectForKey:@"_label"];
580   NSString *low_label  = [dict objectForKey:@"_low-label"];
581   NSString *high_label = [dict objectForKey:@"_high-label"];
582   NSString *type       = [dict objectForKey:@"type"];
583   NSString *arg        = [dict objectForKey:@"arg"];
584   NSString *low        = [dict objectForKey:@"low"];
585   NSString *high       = [dict objectForKey:@"high"];
586   NSString *def        = [dict objectForKey:@"default"];
587   NSString *cvt        = [dict objectForKey:@"convert"];
588   
589   NSAssert1 (arg,  @"no arg in %@", label);
590   NSAssert1 (type, @"no type in %@", label);
591
592   if (! low) {
593     NSAssert1 (0, @"no low in %@", [node name]);
594     return;
595   }
596   if (! high) {
597     NSAssert1 (0, @"no high in %@", [node name]);
598     return;
599   }
600   if (! def) {
601     NSAssert1 (0, @"no default in %@", [node name]);
602     return;
603   }
604   if (cvt && ![cvt isEqualToString:@"invert"]) {
605     NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
606                label);
607   }
608     
609   // If 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   if (!strncmp (wiki, url, strlen(wiki))) {
1051     char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1052     strcpy (anchor, "Wikipedia: \"");
1053     const char *in = url + strlen(wiki);
1054     char *out = anchor + strlen(anchor);
1055     while (*in) {
1056       if (*in == '_') {
1057         *out++ = ' ';
1058       } else if (*in == '#') {
1059         *out++ = ':';
1060         *out++ = ' ';
1061       } else if (*in == '%') {
1062         char hex[3];
1063         hex[0] = in[1];
1064         hex[1] = in[2];
1065         hex[2] = 0;
1066         int n = 0;
1067         sscanf (hex, "%x", &n);
1068         *out++ = (char) n;
1069         in += 2;
1070       } else {
1071         *out++ = *in;
1072       }
1073       in++;
1074     }
1075     *out++ = '"';
1076     *out = 0;
1077     return anchor;
1078
1079   } else {
1080     return strdup (url);
1081   }
1082 }
1083
1084
1085 /* Converts any http: URLs in the given text field to clickable links.
1086  */
1087 static void
1088 hreffify (NSText *nstext)
1089 {
1090   NSString *text = [nstext string];
1091   [nstext setRichText:YES];
1092
1093   int L = [text length];
1094   NSRange start;                // range is start-of-search to end-of-string
1095   start.location = 0;
1096   start.length = L;
1097   while (start.location < L) {
1098
1099     // Find the beginning of a URL...
1100     //
1101     NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1102     if (r2.location == NSNotFound)
1103       break;
1104
1105     // Next time around, start searching after this.
1106     start.location = r2.location + r2.length;
1107     start.length = L - start.location;
1108
1109     // Find the end of a URL (whitespace or EOF)...
1110     //
1111     NSRange r3 = [text rangeOfCharacterFromSet:
1112                          [NSCharacterSet whitespaceAndNewlineCharacterSet]
1113                        options:0 range:start];
1114     if (r3.location == NSNotFound)    // EOF
1115       r3.location = L, r3.length = 0;
1116
1117     // Next time around, start searching after this.
1118     start.location = r3.location;
1119     start.length = L - start.location;
1120
1121     // Set r2 to the start/length of this URL.
1122     r2.length = start.location - r2.location;
1123
1124     // Extract the URL.
1125     NSString *nsurl = [text substringWithRange:r2];
1126     const char *url = [nsurl UTF8String];
1127
1128     // If this is a Wikipedia URL, make the linked text be prettier.
1129     //
1130     char *anchor = anchorize(url);
1131
1132     // Construct the RTF corresponding to <A HREF="url">anchor</A>
1133     //
1134     const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1135     char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1136     sprintf (rtf, fmt, url, anchor);
1137     free (anchor);
1138     NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1139
1140     // Insert the RTF into the NSText.
1141     [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1142
1143     int L2 = [text length];  // might have changed
1144     start.location -= (L - L2);
1145     L = L2;
1146   }
1147 }
1148
1149 /* Makes the text up to the first comma be bold.
1150  */
1151 static void
1152 boldify (NSText *nstext)
1153 {
1154   NSString *text = [nstext string];
1155   NSRange r = [text rangeOfString:@"," options:0];
1156   r.length = r.location+1;
1157   r.location = 0;
1158
1159   NSFont *font = [nstext font];
1160   font = [NSFont boldSystemFontOfSize:[font pointSize]];
1161   [nstext setFont:font range:r];
1162 }
1163
1164
1165 static void layout_group (NSView *group, BOOL horiz_p);
1166
1167
1168 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
1169    wrapped in <hgroup> or <vgroup> in the XML.
1170  */
1171 static void
1172 make_group (NSUserDefaultsController *prefs,
1173             const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, 
1174             BOOL horiz_p)
1175 {
1176   NSRect rect;
1177   rect.size.width = rect.size.height = 1;
1178   rect.origin.x = rect.origin.y = 0;
1179   NSView *group = [[NSView alloc] initWithFrame:rect];
1180   traverse_children (prefs, opts, group, node);
1181
1182   layout_group (group, horiz_p);
1183
1184   rect.size.width = rect.size.height = 0;
1185   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1186   [box setTitlePosition:NSNoTitle];
1187   [box setBorderType:NSNoBorder];
1188   [box setContentViewMargins:rect.size];
1189   [box setContentView:group];
1190   [box sizeToFit];
1191
1192   place_child (parent, box, NO);
1193 }
1194
1195
1196 static void
1197 layout_group (NSView *group, BOOL horiz_p)
1198 {
1199   NSArray *kids = [group subviews];
1200   int nkids = [kids count];
1201   int i;
1202   double maxx = 0, miny = 0;
1203   for (i = 0; i < nkids; i++) {
1204     NSView *kid = [kids objectAtIndex:i];
1205     NSRect r = [kid frame];
1206     
1207     if (horiz_p) {
1208       maxx += r.size.width + COLUMN_SPACING;
1209       if (r.size.height > -miny) miny = -r.size.height;
1210     } else {
1211       if (r.size.width > maxx)  maxx = r.size.width;
1212       miny = r.origin.y - r.size.height;
1213     }
1214   }
1215   
1216   NSRect rect;
1217   rect.size.width = maxx;
1218   rect.size.height = -miny;
1219   [group setFrame:rect];
1220
1221   double x = 0;
1222   for (i = 0; i < nkids; i++) {
1223     NSView *kid = [kids objectAtIndex:i];
1224     NSRect r = [kid frame];
1225     if (horiz_p) {
1226       r.origin.y = rect.size.height - r.size.height;
1227       r.origin.x = x;
1228       x += r.size.width + COLUMN_SPACING;
1229     } else {
1230       r.origin.y -= miny;
1231     }
1232     [kid setFrame:r];
1233   }
1234 }
1235
1236
1237 static void
1238 make_text_controls (NSUserDefaultsController *prefs,
1239                     const XrmOptionDescRec *opts, 
1240                     NSView *parent, NSXMLNode *node)
1241 {
1242   /*
1243     Display Text:
1244      (x)  Computer name and time
1245      ( )  Text       [__________________________]
1246      ( )  Text file  [_________________] [Choose]
1247      ( )  URL        [__________________________]
1248
1249     textMode -text-mode date
1250     textMode -text-mode literal   textLiteral -text-literal %
1251     textMode -text-mode file      textFile    -text-file %
1252     textMode -text-mode url       textURL     -text-url %
1253    */
1254   NSRect rect;
1255   rect.size.width = rect.size.height = 1;
1256   rect.origin.x = rect.origin.y = 0;
1257   NSView *group = [[NSView alloc] initWithFrame:rect];
1258   NSView *rgroup = [[NSView alloc] initWithFrame:rect];
1259
1260
1261   NSXMLElement *node2;
1262   NSView *control;
1263
1264   // This is how you link radio buttons together.
1265   //
1266   NSButtonCell *proto = [[NSButtonCell alloc] init];
1267   [proto setButtonType:NSRadioButton];
1268
1269   rect.origin.x = rect.origin.y = 0;
1270   rect.size.width = rect.size.height = 10;
1271   NSMatrix *matrix = [[NSMatrix alloc] 
1272                        initWithFrame:rect
1273                        mode:NSRadioModeMatrix
1274                        prototype:proto
1275                        numberOfRows:4
1276                        numberOfColumns:1];
1277   [matrix setAllowsEmptySelection:NO];
1278
1279   NSArrayController *cnames  = [[NSArrayController alloc] initWithContent:nil];
1280   [cnames addObject:@"Computer name and time"];
1281   [cnames addObject:@"Text"];
1282   [cnames addObject:@"File"];
1283   [cnames addObject:@"URL"];
1284   [matrix bind:@"content"
1285           toObject:cnames
1286           withKeyPath:@"arrangedObjects"
1287           options:nil];
1288   [cnames release];
1289
1290   bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts);
1291
1292   place_child (group, matrix, NO);
1293   place_child (group, rgroup, YES);
1294
1295   //  <string id="textLiteral" _label="" arg-set="-text-literal %"/>
1296   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1297   [node2 setAttributesAsDictionary:
1298           [NSDictionary dictionaryWithObjectsAndKeys:
1299                         @"textLiteral",        @"id",
1300                         @"-text-literal %",    @"arg",
1301                         nil]];
1302   make_text_field (prefs, opts, rgroup, node2, YES);
1303   [node2 release];
1304
1305   rect = [last_child(rgroup) frame];
1306
1307 /* // trying to make the text fields be enabled only when the checkbox is on..
1308   control = last_child (rgroup);
1309   [control bind:@"enabled"
1310            toObject:[matrix cellAtRow:1 column:0]
1311            withKeyPath:@"value"
1312            options:nil];
1313 */
1314
1315
1316   //  <file id="textFile" _label="" arg-set="-text-file %"/>
1317   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1318   [node2 setAttributesAsDictionary:
1319           [NSDictionary dictionaryWithObjectsAndKeys:
1320                         @"textFile",           @"id",
1321                         @"-text-file %",       @"arg",
1322                         nil]];
1323   make_file_selector (prefs, opts, rgroup, node2, NO, YES);
1324   [node2 release];
1325
1326   rect = [last_child(rgroup) frame];
1327
1328   //  <string id="textURL" _label="" arg-set="text-url %"/>
1329   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1330   [node2 setAttributesAsDictionary:
1331           [NSDictionary dictionaryWithObjectsAndKeys:
1332                         @"textURL",            @"id",
1333                         @"-text-url %",        @"arg",
1334                         nil]];
1335   make_text_field (prefs, opts, rgroup, node2, YES);
1336   [node2 release];
1337
1338   rect = [last_child(rgroup) frame];
1339
1340   layout_group (rgroup, NO);
1341
1342   rect = [rgroup frame];
1343   rect.size.width += 35;    // WTF?  Why is rgroup too narrow?
1344   [rgroup setFrame:rect];
1345
1346
1347   // Set the height of the cells in the radio-box matrix to the height of
1348   // the (last of the) text fields.
1349   control = last_child (rgroup);
1350   rect = [control frame];
1351   rect.size.width = 30;  // width of the string "Text", plus a bit...
1352   rect.size.height += LINE_SPACING;
1353   [matrix setCellSize:rect.size];
1354   [matrix sizeToCells];
1355
1356   layout_group (group, YES);
1357   rect = [matrix frame];
1358   rect.origin.x += rect.size.width + COLUMN_SPACING;
1359   rect.origin.y -= [control frame].size.height - LINE_SPACING;
1360   [rgroup setFrameOrigin:rect.origin];
1361
1362   // now cheat on the size of the matrix: allow it to overlap (underlap)
1363   // the text fields.
1364   // 
1365   rect.size = [matrix cellSize];
1366   rect.size.width *= 10;
1367   [matrix setCellSize:rect.size];
1368   [matrix sizeToCells];
1369
1370   // Cheat on the position of the stuff on the right (the rgroup).
1371   // GAAAH, this code is such crap!
1372   rect = [rgroup frame];
1373   rect.origin.y -= 5;
1374   [rgroup setFrame:rect];
1375
1376
1377   rect.size.width = rect.size.height = 0;
1378   NSBox *box = [[NSBox alloc] initWithFrame:rect];
1379   [box setTitlePosition:NSAtTop];
1380   [box setBorderType:NSBezelBorder];
1381   [box setTitle:@"Display Text"];
1382
1383   rect.size.width = rect.size.height = 12;
1384   [box setContentViewMargins:rect.size];
1385   [box setContentView:group];
1386   [box sizeToFit];
1387
1388   place_child (parent, box, NO);
1389 }
1390
1391
1392 static void
1393 make_image_controls (NSUserDefaultsController *prefs,
1394                      const XrmOptionDescRec *opts, 
1395                      NSView *parent, NSXMLNode *node)
1396 {
1397   /*
1398     [x]  Grab desktop images
1399     [ ]  Choose random image:
1400          [__________________________]  [Choose]
1401
1402    <boolean id="grabDesktopImages" _label="Grab desktop images"
1403        arg-unset="-no-grab-desktop"/>
1404    <boolean id="chooseRandomImages" _label="Grab desktop images"
1405        arg-unset="-choose-random-images"/>
1406    <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
1407    */
1408
1409   NSXMLElement *node2;
1410
1411   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1412   [node2 setAttributesAsDictionary:
1413           [NSDictionary dictionaryWithObjectsAndKeys:
1414                         @"grabDesktopImages",   @"id",
1415                         @"Grab desktop images", @"_label",
1416                         @"-no-grab-desktop",    @"arg-unset",
1417                         nil]];
1418   make_checkbox (prefs, opts, parent, node2);
1419   [node2 release];
1420
1421   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
1422   [node2 setAttributesAsDictionary:
1423           [NSDictionary dictionaryWithObjectsAndKeys:
1424                         @"chooseRandomImages",    @"id",
1425                         @"Choose random images",  @"_label",
1426                         @"-choose-random-images", @"arg-set",
1427                         nil]];
1428   make_checkbox (prefs, opts, parent, node2);
1429   [node2 release];
1430
1431   node2 = [[NSXMLElement alloc] initWithName:@"string"];
1432   [node2 setAttributesAsDictionary:
1433           [NSDictionary dictionaryWithObjectsAndKeys:
1434                         @"imageDirectory",     @"id",
1435                         @"Images directory:",  @"_label",
1436                         @"-image-directory %", @"arg",
1437                         nil]];
1438   make_file_selector (prefs, opts, parent, node2, YES, NO);
1439   [node2 release];
1440 }
1441
1442
1443
1444 /* Create some kind of control corresponding to the given XML node.
1445  */
1446 static void
1447 make_control (NSUserDefaultsController *prefs,
1448               const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
1449 {
1450   NSString *name = [node name];
1451
1452   if ([node kind] == NSXMLCommentKind)
1453     return;
1454   if ([node kind] != NSXMLElementKind) {
1455     NSAssert2 (0, @"weird XML node kind: %d: %@", [node kind], node);
1456     return;
1457   }
1458
1459   if ([name isEqualToString:@"hgroup"] ||
1460       [name isEqualToString:@"vgroup"]) {
1461
1462     BOOL horiz_p = [name isEqualToString:@"hgroup"];
1463     make_group (prefs, opts, parent, node, horiz_p);
1464
1465   } else if ([name isEqualToString:@"command"]) {
1466     // do nothing: this is the "-root" business
1467
1468   } else if ([name isEqualToString:@"boolean"]) {
1469     make_checkbox (prefs, opts, parent, node);
1470
1471   } else if ([name isEqualToString:@"string"]) {
1472     make_text_field (prefs, opts, parent, node, NO);
1473
1474   } else if ([name isEqualToString:@"file"]) {
1475     make_file_selector (prefs, opts, parent, node, NO, NO);
1476
1477   } else if ([name isEqualToString:@"number"]) {
1478     make_number_selector (prefs, opts, parent, node);
1479
1480   } else if ([name isEqualToString:@"select"]) {
1481     make_option_menu (prefs, opts, parent, node);
1482
1483   } else if ([name isEqualToString:@"_description"]) {
1484     make_desc_label (parent, node);
1485
1486   } else if ([name isEqualToString:@"xscreensaver-text"]) {
1487     make_text_controls (prefs, opts, parent, node);
1488
1489   } else if ([name isEqualToString:@"xscreensaver-image"]) {
1490     make_image_controls (prefs, opts, parent, node);
1491
1492   } else {
1493     NSAssert1 (0, @"unknown tag: %@", name);
1494   }
1495 }
1496
1497
1498 /* Iterate over and process the children of this XML node.
1499  */
1500 static void
1501 traverse_children (NSUserDefaultsController *prefs,
1502                    const XrmOptionDescRec *opts,
1503                    NSView *parent, NSXMLNode *node)
1504 {
1505   NSArray *children = [node children];
1506   int i, count = [children count];
1507   for (i = 0; i < count; i++) {
1508     NSXMLNode *child = [children objectAtIndex:i];
1509     make_control (prefs, opts, parent, child);
1510   }
1511 }
1512
1513 /* Handle the options on the top level <xscreensaver> tag.
1514  */
1515 static void
1516 parse_xscreensaver_tag (NSXMLNode *node)
1517 {
1518   NSMutableDictionary *dict =
1519   [NSMutableDictionary dictionaryWithObjectsAndKeys:
1520     @"", @"name",
1521     @"", @"_label",
1522     nil];
1523   parse_attrs (dict, node);
1524   NSString *name  = [dict objectForKey:@"name"];
1525   NSString *label = [dict objectForKey:@"_label"];
1526     
1527   if (!label) {
1528     NSAssert1 (0, @"no _label in %@", [node name]);
1529     return;
1530   }
1531   if (!name) {
1532     NSAssert1 (0, @"no name in \"%@\"", label);
1533     return;
1534   }
1535   
1536   // #### do any callers need the "name" field for anything?
1537 }
1538
1539
1540 /* Kludgey magic to make the window enclose the controls we created.
1541  */
1542 static void
1543 fix_contentview_size (NSView *parent)
1544 {
1545   NSRect f;
1546   NSArray *kids = [parent subviews];
1547   int nkids = [kids count];
1548   NSView *text;  // the NSText at the bottom of the window
1549   NSView *last;  // the last child before the NSText
1550   double maxx = 0, miny = 0;
1551   int i;
1552
1553   /* Find the size of the rectangle taken up by each of the children
1554      except the final "NSText" child.
1555   */
1556   for (i = 0; i < nkids; i++) {
1557     NSView *kid = [kids objectAtIndex:i];
1558     if ([kid isKindOfClass:[NSText class]]) {
1559       text = kid;
1560       continue;
1561     }
1562     f = [kid frame];
1563     if (f.origin.x + f.size.width > maxx)  maxx = f.origin.x + f.size.width;
1564     if (f.origin.y - f.size.height < miny) miny = f.origin.y;
1565     last = kid;
1566 //    NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1567 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1568 //          f.origin.y + f.size.height, [kid class]);
1569   }
1570   
1571   if (maxx < 400) maxx = 400;   // leave room for the NSText paragraph...
1572   
1573   /* Now that we know the width of the window, set the width of the NSText to
1574      that, so that it can decide what its height needs to be.
1575    */
1576   f = [text frame];
1577 //  NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1578 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1579 //        f.origin.y + f.size.height, [text class]);
1580   
1581   // set the NSText's width (this changes its height).
1582   f.size.width = maxx - LEFT_MARGIN;
1583   [text setFrame:f];
1584   
1585   // position the NSText below the last child (this gives us a new miny).
1586   f = [text frame];
1587   f.origin.y = miny - f.size.height - LINE_SPACING;
1588   miny = f.origin.y - LINE_SPACING;
1589   [text setFrame:f];
1590   
1591   // Lock the width of the field and unlock the height, and let it resize
1592   // once more, to compute the proper height of the text for that width.
1593   //
1594   [(NSText *) text setHorizontallyResizable:NO];
1595   [(NSText *) text setVerticallyResizable:YES];
1596   [(NSText *) text sizeToFit];
1597
1598   // Now lock the height too: no more resizing this text field.
1599   //
1600   [(NSText *) text setVerticallyResizable:NO];
1601
1602   // Now reposition the top edge of the text field to be back where it
1603   // was before we changed the height.
1604   //
1605   float oh = f.size.height;
1606   f = [text frame];
1607   float dh = f.size.height - oh;
1608   f.origin.y += dh;
1609   [text setFrame:f];
1610
1611   // Also adjust the parent height by the change in height of the text field.
1612   miny -= dh;
1613
1614 //  NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1615 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
1616 //        f.origin.y + f.size.height, [text class]);
1617   
1618   
1619   /* Set the contentView to the size of the children.
1620    */
1621   f = [parent frame];
1622   float yoff = f.size.height;
1623   f.size.width = maxx + LEFT_MARGIN;
1624   f.size.height = -(miny - LEFT_MARGIN*2);
1625   yoff = f.size.height - yoff;
1626   [parent setFrame:f];
1627
1628 //  NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", 
1629 //        f.size.width, f.size.height, f.origin.x, f.origin.y);
1630
1631   /* Now move all of the kids up into the window.
1632    */
1633   f = [parent frame];
1634   float shift = f.size.height;
1635 //  NSLog(@"shift: %3.0f", shift);
1636   for (i = 0; i < nkids; i++) {
1637     NSView *kid = [kids objectAtIndex:i];
1638     f = [kid frame];
1639     f.origin.y += shift;
1640     [kid setFrame:f];
1641 //    NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
1642 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
1643 //          f.origin.y + f.size.height, [kid class]);
1644   }
1645   
1646   /* Set the kids to track the top left corner of the window when resized.
1647      Set the NSText to track the bottom right corner as well.
1648    */
1649   for (i = 0; i < nkids; i++) {
1650     NSView *kid = [kids objectAtIndex:i];
1651     unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
1652     if ([kid isKindOfClass:[NSText class]])
1653       mask |= NSViewWidthSizable|NSViewHeightSizable;
1654     [kid setAutoresizingMask:mask];
1655   }
1656 }
1657
1658
1659 - (void) okClicked:(NSObject *)arg
1660 {
1661   [userDefaultsController commitEditing];
1662   [userDefaultsController save:self];
1663   [NSApp endSheet:self returnCode:NSOKButton];
1664   [self close];
1665 }
1666
1667 - (void) cancelClicked:(NSObject *)arg
1668 {
1669   [userDefaultsController revert:self];
1670   [NSApp endSheet:self returnCode:NSCancelButton];
1671   [self close];
1672 }
1673
1674 - (void) resetClicked:(NSObject *)arg
1675 {
1676   [userDefaultsController revertToInitialValues:self];
1677 }
1678
1679
1680 static NSView *
1681 wrap_with_buttons (NSWindow *window, NSView *panel)
1682 {
1683   NSRect rect;
1684   
1685   // Make a box to hold the buttons at the bottom of the window.
1686   //
1687   rect = [panel frame];
1688   rect.origin.x = rect.origin.y = 0;
1689   rect.size.height = 10;
1690   NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
1691   [bbox setTitlePosition:NSNoTitle];  
1692   [bbox setBorderType:NSNoBorder];
1693   
1694   // Make some buttons: Default, Cancel, OK
1695   //
1696   rect.origin.x = rect.origin.y = 0;
1697   rect.size.width = rect.size.height = 10;
1698   NSButton *reset = [[NSButton alloc] initWithFrame:rect];
1699   [reset setTitle:@"Reset to Defaults"];
1700   [reset setBezelStyle:NSRoundedBezelStyle];
1701   [reset sizeToFit];
1702
1703   rect = [reset frame];
1704   NSButton *ok = [[NSButton alloc] initWithFrame:rect];
1705   [ok setTitle:@"OK"];
1706   [ok setBezelStyle:NSRoundedBezelStyle];
1707   [ok sizeToFit];
1708   rect = [bbox frame];
1709   rect.origin.x = rect.size.width - [ok frame].size.width;
1710   [ok setFrameOrigin:rect.origin];
1711
1712   rect = [ok frame];
1713   NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
1714   [cancel setTitle:@"Cancel"];
1715   [cancel setBezelStyle:NSRoundedBezelStyle];
1716   [cancel sizeToFit];
1717   rect.origin.x -= [cancel frame].size.width + 10;
1718   [cancel setFrameOrigin:rect.origin];
1719
1720   // Bind OK to RET and Cancel to ESC.
1721   [ok     setKeyEquivalent:@"\r"];
1722   [cancel setKeyEquivalent:@"\e"];
1723
1724   // The correct width for OK and Cancel buttons is 68 pixels
1725   // ("Human Interface Guidelines: Controls: Buttons: 
1726   // Push Button Specifications").
1727   //
1728   rect = [ok frame];
1729   rect.size.width = 68;
1730   [ok setFrame:rect];
1731
1732   rect = [cancel frame];
1733   rect.size.width = 68;
1734   [cancel setFrame:rect];
1735
1736   // It puts the buttons in the box or else it gets the hose again
1737   //
1738   [bbox addSubview:ok];
1739   [bbox addSubview:cancel];
1740   [bbox addSubview:reset];
1741   [bbox sizeToFit];
1742   
1743   // make a box to hold the button-box, and the preferences view
1744   //
1745   rect = [bbox frame];
1746   rect.origin.y += rect.size.height;
1747   NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
1748   [pbox setTitlePosition:NSNoTitle];
1749   [pbox setBorderType:NSBezelBorder];
1750
1751   // Enforce a max height on the dialog, so that it's obvious to me
1752   // (on a big screen) when the dialog will fall off the bottom of
1753   // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
1754   {
1755     NSRect f = [panel frame];
1756     int screen_height = (768    // shortest "modern" Mac display
1757                          - 22   // menu bar
1758                          - 56   // System Preferences toolbar
1759                          - 140  // default magnified bottom dock icon
1760                          );
1761     if (f.size.height > screen_height) {
1762       NSLog(@"%@ height was %.0f; clipping to %d", 
1763           [panel class], f.size.height, screen_height);
1764       f.size.height = screen_height;
1765       [panel setFrame:f];
1766     }
1767   }
1768
1769   [pbox addSubview:panel];
1770   [pbox addSubview:bbox];
1771   [pbox sizeToFit];
1772
1773   [reset  setAutoresizingMask:NSViewMaxXMargin];
1774   [cancel setAutoresizingMask:NSViewMinXMargin];
1775   [ok     setAutoresizingMask:NSViewMinXMargin];
1776   [bbox   setAutoresizingMask:NSViewWidthSizable];
1777   
1778   // grab the clicks
1779   //
1780   [ok     setTarget:window];
1781   [cancel setTarget:window];
1782   [reset  setTarget:window];
1783   [ok     setAction:@selector(okClicked:)];
1784   [cancel setAction:@selector(cancelClicked:)];
1785   [reset  setAction:@selector(resetClicked:)];
1786   
1787   return pbox;
1788 }
1789
1790
1791 /* Iterate over and process the children of the root node of the XML document.
1792  */
1793 static void
1794 traverse_tree (NSUserDefaultsController *prefs,
1795                NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node)
1796 {
1797   if (![[node name] isEqualToString:@"screensaver"]) {
1798     NSAssert (0, @"top level node is not <xscreensaver>");
1799   }
1800
1801   parse_xscreensaver_tag (node);
1802   
1803   NSRect rect;
1804   rect.origin.x = rect.origin.y = 0;
1805   rect.size.width = rect.size.height = 1;
1806
1807   NSView *panel = [[NSView alloc] initWithFrame:rect];
1808   
1809   traverse_children (prefs, opts, panel, node);
1810   fix_contentview_size (panel);
1811
1812   NSView *root = wrap_with_buttons (window, panel);
1813   [prefs setAppliesImmediately:NO];
1814
1815   [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1816
1817   rect = [window frameRectForContentRect:[root frame]];
1818   [window setFrame:rect display:NO];
1819   [window setMinSize:rect.size];
1820   
1821   [window setContentView:root];
1822 }
1823
1824
1825 /* When this object is instantiated, it parses the XML file and creates
1826    controls on itself that are hooked up to the appropriate preferences.
1827    The default size of the view is just big enough to hold them all.
1828  */
1829 - (id)initWithXMLFile: (NSString *) xml_file
1830               options: (const XrmOptionDescRec *) opts
1831            controller: (NSUserDefaultsController *) prefs
1832 {
1833   if (! (self = [super init]))
1834     return 0;
1835
1836   // instance variable
1837   userDefaultsController = prefs;
1838   [prefs retain];
1839
1840   NSURL *furl = [NSURL fileURLWithPath:xml_file];
1841
1842   if (!furl) {
1843     NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
1844     return nil;
1845   }
1846
1847   NSError *err = nil;
1848   NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] 
1849                             initWithContentsOfURL:furl
1850                             options:(NSXMLNodePreserveWhitespace |
1851                                      NSXMLNodePreserveCDATA)
1852                             error:&err];
1853 /* clean up?
1854     if (!xmlDoc) {
1855       xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl
1856                                       options:NSXMLDocumentTidyXML
1857                                       error:&err];
1858     }
1859 */
1860   if (!xmlDoc || err) {
1861     if (err)
1862       NSAssert2 (0, @"XML Error: %@: %@",
1863                  xml_file, [err localizedDescription]);
1864     return nil;
1865   }
1866
1867   traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
1868
1869   return self;
1870 }
1871
1872
1873 - (void) dealloc
1874 {
1875   [userDefaultsController release];
1876   [super dealloc];
1877 }
1878
1879 @end