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