From http://www.jwz.org/xscreensaver/xscreensaver-5.37.tar.gz
[xscreensaver] / OSX / XScreenSaverConfigSheet.m
1 /* xscreensaver, Copyright (c) 2006-2017 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 #import "Updater.h"
28
29 #import "jwxyz.h"
30 #import "InvertedSlider.h"
31
32 #ifdef USE_IPHONE
33 # define NSView      UIView
34 # define NSRect      CGRect
35 # define NSSize      CGSize
36 # define NSTextField UITextField
37 # define NSButton    UIButton
38 # define NSFont      UIFont
39 # define NSStepper   UIStepper
40 # define NSMenuItem  UIMenuItem
41 # define NSText      UILabel
42 # define minValue    minimumValue
43 # define maxValue    maximumValue
44 # define setMinValue setMinimumValue
45 # define setMaxValue setMaximumValue
46 # define LABEL       UILabel
47 #else
48 # define LABEL       NSTextField
49 #endif // USE_IPHONE
50
51 #undef LABEL_ABOVE_SLIDER
52 #define USE_HTML_LABELS
53
54
55 #pragma mark XML Parser
56
57 /* I used to use the "NSXMLDocument" XML parser, but that doesn't exist
58    on iOS.  The "NSXMLParser" parser exists on both OSX and iOS, so I
59    converted to use that.  However, to avoid having to re-write all of
60    the old code, I faked out a halfassed implementation of the
61    "NSXMLNode" class that "NSXMLDocument" used to return.
62  */
63
64 #define NSXMLNode          SimpleXMLNode
65 #define NSXMLElement       SimpleXMLNode
66 #define NSXMLCommentKind   SimpleXMLCommentKind
67 #define NSXMLElementKind   SimpleXMLElementKind
68 #define NSXMLAttributeKind SimpleXMLAttributeKind
69 #define NSXMLTextKind      SimpleXMLTextKind
70
71 typedef enum { SimpleXMLCommentKind,
72                SimpleXMLElementKind,
73                SimpleXMLAttributeKind,
74                SimpleXMLTextKind,
75 } SimpleXMLKind;
76
77 @interface SimpleXMLNode : NSObject
78 {
79   SimpleXMLKind kind;
80   NSString *name;
81   SimpleXMLNode *parent;
82   NSMutableArray *children;
83   NSMutableArray *attributes;
84   id object;
85 }
86
87 @property(nonatomic) SimpleXMLKind kind;
88 @property(nonatomic, retain) NSString *name;
89 @property(nonatomic, retain) SimpleXMLNode *parent;
90 @property(nonatomic, retain) NSMutableArray *children;
91 @property(nonatomic, retain) NSMutableArray *attributes;
92 @property(nonatomic, retain, getter=objectValue, setter=setObjectValue:)
93   id object;
94
95 @end
96
97 @implementation SimpleXMLNode
98
99 @synthesize kind;
100 @synthesize name;
101 //@synthesize parent;
102 @synthesize children;
103 @synthesize attributes;
104 @synthesize object;
105
106 - (id) init
107 {
108   self = [super init];
109   attributes = [NSMutableArray arrayWithCapacity:10];
110   return self;
111 }
112
113
114 - (id) initWithName:(NSString *)n
115 {
116   self = [self init];
117   [self setKind:NSXMLElementKind];
118   [self setName:n];
119   return self;
120 }
121
122
123 - (void) setAttributesAsDictionary:(NSDictionary *)dict
124 {
125   for (NSString *key in dict) {
126     NSObject *val = [dict objectForKey:key];
127     SimpleXMLNode *n = [[SimpleXMLNode alloc] init];
128     [n setKind:SimpleXMLAttributeKind];
129     [n setName:key];
130     [n setObjectValue:val];
131     [attributes addObject:n];
132     [n release];
133   }
134 }
135
136 - (SimpleXMLNode *) parent { return parent; }
137
138 - (void) setParent:(SimpleXMLNode *)p
139 {
140   NSAssert (!parent, @"parent already set");
141   if (!p) return;
142   parent = p;
143   NSMutableArray *kids = [p children];
144   if (!kids) {
145     kids = [NSMutableArray arrayWithCapacity:10];
146     [p setChildren:kids];
147   }
148   [kids addObject:self];
149 }
150 @end
151
152
153 #pragma mark textMode value transformer
154
155 // A value transformer for mapping "url" to "3" and vice versa in the
156 // "textMode" preference, since NSMatrix uses NSInteger selectedIndex.
157
158 #ifndef USE_IPHONE
159 @interface TextModeTransformer: NSValueTransformer {}
160 @end
161 @implementation TextModeTransformer
162 + (Class)transformedValueClass { return [NSString class]; }
163 + (BOOL)allowsReverseTransformation { return YES; }
164
165 - (id)transformedValue:(id)value {
166   if ([value isKindOfClass:[NSString class]]) {
167     int i = -1;
168     if      ([value isEqualToString:@"date"])    { i = 0; }
169     else if ([value isEqualToString:@"literal"]) { i = 1; }
170     else if ([value isEqualToString:@"file"])    { i = 2; }
171     else if ([value isEqualToString:@"url"])     { i = 3; }
172     else if ([value isEqualToString:@"program"]) { i = 4; }
173     if (i != -1)
174       value = [NSNumber numberWithInt: i];
175   }
176   return value;
177 }
178
179 - (id)reverseTransformedValue:(id)value {
180   if ([value isKindOfClass:[NSNumber class]]) {
181     switch ((int) [value doubleValue]) {
182     case 0: value = @"date";    break;
183     case 1: value = @"literal"; break;
184     case 2: value = @"file";    break;
185     case 3: value = @"url";     break;
186     case 4: value = @"program"; break;
187     }
188   }
189   return value;
190 }
191 @end
192 #endif // USE_IPHONE
193
194
195 #pragma mark Implementing radio buttons
196
197 /* The UIPickerView is a hideous and uncustomizable piece of shit.
198    I can't believe Apple actually released that thing on the world.
199    Let's fake up some radio buttons instead.
200  */
201
202 #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
203
204 @interface RadioButton : UILabel
205 {
206   int index;
207   NSArray *items;
208 }
209
210 @property(nonatomic) int index;
211 @property(nonatomic, retain) NSArray *items;
212
213 @end
214
215 @implementation RadioButton
216
217 @synthesize index;
218 @synthesize items;
219
220 - (id) initWithIndex:(int)_index items:_items
221 {
222   self = [super initWithFrame:CGRectZero];
223   index = _index;
224   items = [_items retain];
225
226   [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
227   [self setBackgroundColor:[UIColor clearColor]];
228   [self sizeToFit];
229
230   return self;
231 }
232
233 @end
234
235
236 # endif // !USE_PICKER_VIEW
237
238
239 # pragma mark Implementing labels with clickable links
240
241 #if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
242
243 @interface HTMLLabel : UIView <UIWebViewDelegate>
244 {
245   NSString *html;
246   UIFont *font;
247   UIWebView *webView;
248 }
249
250 @property(nonatomic, retain) NSString *html;
251 @property(nonatomic, retain) UIWebView *webView;
252
253 - (id) initWithHTML:(NSString *)h font:(UIFont *)f;
254 - (id) initWithText:(NSString *)t font:(UIFont *)f;
255 - (void) setHTML:(NSString *)h;
256 - (void) setText:(NSString *)t;
257 - (void) sizeToFit;
258
259 @end
260
261 @implementation HTMLLabel
262
263 @synthesize html;
264 @synthesize webView;
265
266 - (id) initWithHTML:(NSString *)h font:(UIFont *)f
267 {
268   self = [super init];
269   if (! self) return 0;
270   font = [f retain];
271   webView = [[UIWebView alloc] init];
272   webView.delegate = self;
273   webView.dataDetectorTypes = UIDataDetectorTypeNone;
274   self.   autoresizingMask = UIViewAutoresizingNone;  // we do it manually
275   webView.autoresizingMask = UIViewAutoresizingNone;
276   webView.scrollView.scrollEnabled = NO; 
277   webView.scrollView.bounces = NO;
278   webView.opaque = NO;
279   [webView setBackgroundColor:[UIColor clearColor]];
280
281   [self addSubview: webView];
282   [self setHTML: h];
283   return self;
284 }
285
286 - (id) initWithText:(NSString *)t font:(UIFont *)f
287 {
288   self = [self initWithHTML:@"" font:f];
289   if (! self) return 0;
290   [self setText: t];
291   return self;
292 }
293
294
295 - (void) setHTML: (NSString *)h
296 {
297   if (! h) return;
298   [h retain];
299   if (html) [html release];
300   html = h;
301   NSString *h2 =
302     [NSString stringWithFormat:
303                 @"<!DOCTYPE HTML PUBLIC "
304                    "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
305                    " \"http://www.w3.org/TR/html4/loose.dtd\">"
306                  "<HTML>"
307                   "<HEAD>"
308 //                   "<META NAME=\"viewport\" CONTENT=\""
309 //                      "width=device-width"
310 //                      "initial-scale=1.0;"
311 //                      "maximum-scale=1.0;\">"
312                    "<STYLE>"
313                     "<!--\n"
314                       "body {"
315                       " margin: 0; padding: 0; border: 0;"
316                       " font-family: \"%@\";"
317                       " font-size: %.4fpx;"     // Must be "px", not "pt"!
318                       " line-height: %.4fpx;"   // And no spaces before it.
319                       " -webkit-text-size-adjust: none;"
320                       "}"
321                     "\n//-->\n"
322                    "</STYLE>"
323                   "</HEAD>"
324                   "<BODY>"
325                    "%@"
326                   "</BODY>"
327                  "</HTML>",
328               [font fontName],
329               [font pointSize],
330               [font lineHeight],
331               h];
332   [webView stopLoading];
333   [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
334 }
335
336
337 static char *anchorize (const char *url);
338
339 - (void) setText: (NSString *)t
340 {
341   t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
342                                            whitespaceCharacterSet]];
343   t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&amp;"];
344   t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"];
345   t = [t stringByReplacingOccurrencesOfString:@">" withString:@"&gt;"];
346   t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
347   t = [t stringByReplacingOccurrencesOfString:@"<P>  "
348          withString:@"<P> &nbsp; &nbsp; &nbsp; &nbsp; "];
349   t = [t stringByReplacingOccurrencesOfString:@"\n "
350          withString:@"<BR> &nbsp; &nbsp; &nbsp; &nbsp; "];
351
352   NSString *h = @"";
353   for (NSString *s in
354          [t componentsSeparatedByCharactersInSet:
355               [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
356     if ([s hasPrefix:@"http://"] ||
357         [s hasPrefix:@"https://"]) {
358       char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
359       NSString *a2 = [NSString stringWithCString: anchor
360                                encoding: NSUTF8StringEncoding];
361       s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
362       free (anchor);
363     }
364     h = [NSString stringWithFormat: @"%@ %@", h, s];
365   }
366
367   h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"];
368   h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"];
369   h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
370                                            whitespaceAndNewlineCharacterSet]];
371
372   [self setHTML: h];
373 }
374
375
376 -(BOOL) webView:(UIWebView *)wv
377         shouldStartLoadWithRequest:(NSURLRequest *)req
378         navigationType:(UIWebViewNavigationType)type
379 {
380   // Force clicked links to open in Safari, not in this window.
381   if (type == UIWebViewNavigationTypeLinkClicked) {
382     [[UIApplication sharedApplication] openURL:[req URL]];
383     return NO;
384   }
385   return YES;
386 }
387
388
389 - (void) setFrame: (CGRect)r
390 {
391   [super setFrame: r];
392   r.origin.x = 0;
393   r.origin.y = 0;
394   [webView setFrame: r];
395 }
396
397
398 - (NSString *) stripTags:(NSString *)str
399 {
400   NSString *result = @"";
401
402   // Add newlines.
403   str = [str stringByReplacingOccurrencesOfString:@"<P>"
404              withString:@"<BR><BR>"
405              options:NSCaseInsensitiveSearch
406              range:NSMakeRange(0, [str length])];
407   str = [str stringByReplacingOccurrencesOfString:@"<BR>"
408              withString:@"\n"
409              options:NSCaseInsensitiveSearch
410              range:NSMakeRange(0, [str length])];
411
412   // Remove HREFs.
413   for (NSString *s in [str componentsSeparatedByString: @"<"]) {
414     NSRange r = [s rangeOfString:@">"];
415     if (r.length > 0)
416       s = [s substringFromIndex: r.location + r.length];
417     result = [result stringByAppendingString: s];
418   }
419
420   // Compress internal horizontal whitespace.
421   str = result;
422   result = @"";
423   for (NSString *s in [str componentsSeparatedByCharactersInSet:
424                              [NSCharacterSet whitespaceCharacterSet]]) {
425     if ([result length] == 0)
426       result = s;
427     else if ([s length] > 0)
428       result = [NSString stringWithFormat: @"%@ %@", result, s];
429   }
430
431   return result;
432 }
433
434
435 - (void) sizeToFit
436 {
437   CGRect r = [self frame];
438
439   /* It would be sensible to just ask the UIWebView how tall the page is,
440      instead of hoping that NSString and UIWebView measure fonts and do
441      wrapping in exactly the same way, but since UIWebView is asynchronous,
442      we'd have to wait for the document to load first, e.g.:
443
444        - Start the document loading;
445        - return a default height to use for the UITableViewCell;
446        - wait for the webViewDidFinishLoad delegate method to fire;
447        - then force the UITableView to reload, to pick up the new height.
448
449      But I couldn't make that work.
450    */
451 # if 0
452   r.size.height = [[webView
453                      stringByEvaluatingJavaScriptFromString:
454                        @"document.body.offsetHeight"]
455                     doubleValue];
456 # else
457   NSString *text = [self stripTags: html];
458   CGSize s = r.size;
459   s.height = 999999;
460   s = [text boundingRectWithSize:s
461                          options:NSStringDrawingUsesLineFragmentOrigin
462                       attributes:@{NSFontAttributeName: font}
463                          context:nil].size;
464   r.size.height = s.height;
465 # endif
466
467   [self setFrame: r];
468 }
469
470
471 - (void) dealloc
472 {
473   [html release];
474   [font release];
475   [webView release];
476   [super dealloc];
477 }
478
479 @end
480
481 #endif // USE_IPHONE && USE_HTML_LABELS
482
483
484 @interface XScreenSaverConfigSheet (Private)
485
486 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
487
488 # ifndef USE_IPHONE
489 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
490 - (void) placeChild: (NSView *)c on:(NSView *)p;
491 static NSView *last_child (NSView *parent);
492 static void layout_group (NSView *group, BOOL horiz_p);
493 # else // USE_IPHONE
494 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
495 - (void) placeChild: (NSObject *)c on:(NSView *)p;
496 - (void) placeSeparator;
497 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
498 - (void) refreshTableView;
499 # endif // USE_IPHONE
500
501 @end
502
503
504 @implementation XScreenSaverConfigSheet
505
506 # define LEFT_MARGIN      20   // left edge of window
507 # define COLUMN_SPACING   10   // gap between e.g. labels and text fields
508 # define LEFT_LABEL_WIDTH 70   // width of all left labels
509 # define LINE_SPACING     10   // leading between each line
510
511 # define FONT_SIZE        17   // Magic hardcoded UITableView font size.
512
513 #pragma mark Talking to the resource database
514
515
516 /* Normally we read resources by looking up "KEY" in the database
517    "org.jwz.xscreensaver.SAVERNAME".  But in the all-in-one iPhone
518    app, everything is stored in the database "org.jwz.xscreensaver"
519    instead, so transform keys to "SAVERNAME.KEY".
520
521    NOTE: This is duplicated in PrefsReader.m, cause I suck.
522  */
523 - (NSString *) makeKey:(NSString *)key
524 {
525 # ifdef USE_IPHONE
526   NSString *prefix = [saver_name stringByAppendingString:@"."];
527   if (! [key hasPrefix:prefix])  // Don't double up!
528     key = [prefix stringByAppendingString:key];
529 # endif
530   return key;
531 }
532
533
534 - (NSString *) makeCKey:(const char *)key
535 {
536   return [self makeKey:[NSString stringWithCString:key
537                                  encoding:NSUTF8StringEncoding]];
538 }
539
540
541 /* Given a command-line option, returns the corresponding resource name.
542    Any arguments in the switch string are ignored (e.g., "-foo x").
543  */
544 - (NSString *) switchToResource:(NSString *)cmdline_switch
545                            opts:(const XrmOptionDescRec *)opts_array
546                          valRet:(NSString **)val_ret
547 {
548   char buf[1280];
549   char *tail = 0;
550   NSAssert(cmdline_switch, @"cmdline switch is null");
551   if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
552                           encoding:NSUTF8StringEncoding]) {
553     NSAssert1(0, @"unable to convert %@", cmdline_switch);
554     return 0;
555   }
556   char *s = strpbrk(buf, " \t\r\n");
557   if (s && *s) {
558     *s = 0;
559     tail = s+1;
560     while (*tail && (*tail == ' ' || *tail == '\t'))
561       tail++;
562   }
563   
564   while (opts_array[0].option) {
565     if (!strcmp (opts_array[0].option, buf)) {
566       const char *ret = 0;
567
568       if (opts_array[0].argKind == XrmoptionNoArg) {
569         if (tail && *tail)
570           NSAssert1 (0, @"expected no args to switch: \"%@\"",
571                      cmdline_switch);
572         ret = opts_array[0].value;
573       } else {
574         if (!tail || !*tail)
575           NSAssert1 (0, @"expected args to switch: \"%@\"",
576                      cmdline_switch);
577         ret = tail;
578       }
579
580       if (val_ret)
581         *val_ret = (ret
582                     ? [NSString stringWithCString:ret
583                                          encoding:NSUTF8StringEncoding]
584                     : 0);
585       
586       const char *res = opts_array[0].specifier;
587       while (*res && (*res == '.' || *res == '*'))
588         res++;
589       return [self makeCKey:res];
590     }
591     opts_array++;
592   }
593   
594   NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
595   return 0;
596 }
597
598
599 - (NSUserDefaultsController *)controllerForKey:(NSString *)key
600 {
601   static NSDictionary *a = 0;
602   if (! a) {
603     a = UPDATER_DEFAULTS;
604     [a retain];
605   }
606   if ([a objectForKey:key])
607     // These preferences are global to all xscreensavers.
608     return globalDefaultsController;
609   else
610     // All other preferences are per-saver.
611     return userDefaultsController;
612 }
613
614
615 #ifdef USE_IPHONE
616
617 // Called when a slider is bonked.
618 //
619 - (void)sliderAction:(UISlider*)sender
620 {
621   if ([active_text_field canResignFirstResponder])
622     [active_text_field resignFirstResponder];
623   NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
624
625   // Hacky API. See comment in InvertedSlider.m.
626   double v = ([sender isKindOfClass: [InvertedSlider class]]
627               ? [(InvertedSlider *) sender transformedValue]
628               : [sender value]);
629
630   [[self controllerForKey:pref_key]
631     setObject:((v == (int) v)
632                ? [NSNumber numberWithInt:(int) v]
633                : [NSNumber numberWithDouble: v])
634     forKey:pref_key];
635 }
636
637 // Called when a checkbox/switch is bonked.
638 //
639 - (void)switchAction:(UISwitch*)sender
640 {
641   if ([active_text_field canResignFirstResponder])
642     [active_text_field resignFirstResponder];
643   NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
644   NSString *v = ([sender isOn] ? @"true" : @"false");
645   [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
646 }
647
648 # ifdef USE_PICKER_VIEW
649 // Called when a picker is bonked.
650 //
651 - (void)pickerView:(UIPickerView *)pv
652         didSelectRow:(NSInteger)row
653         inComponent:(NSInteger)column
654 {
655   if ([active_text_field canResignFirstResponder])
656     [active_text_field resignFirstResponder];
657
658   NSAssert (column == 0, @"internal error");
659   NSArray *a = [picker_values objectAtIndex: [pv tag]];
660   if (! a) return;  // Too early?
661   a = [a objectAtIndex:row];
662   NSAssert (a, @"missing row");
663
664 //NSString *label    = [a objectAtIndex:0];
665   NSString *pref_key = [a objectAtIndex:1];
666   NSObject *pref_val = [a objectAtIndex:2];
667   [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
668 }
669 # else  // !USE_PICKER_VIEW
670
671 // Called when a RadioButton is bonked.
672 //
673 - (void)radioAction:(RadioButton*)sender
674 {
675   if ([active_text_field canResignFirstResponder])
676     [active_text_field resignFirstResponder];
677
678   NSArray *item = [[sender items] objectAtIndex: [sender index]];
679   NSString *pref_key = [item objectAtIndex:1];
680   NSObject *pref_val = [item objectAtIndex:2];
681   [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
682 }
683
684 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
685 {
686   active_text_field = tf;
687   return YES;
688 }
689
690 - (void)textFieldDidEndEditing:(UITextField *)tf
691 {
692   NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
693   NSString *txt = [tf text];
694   [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
695 }
696
697 - (BOOL)textFieldShouldReturn:(UITextField *)tf
698 {
699   active_text_field = nil;
700   [tf resignFirstResponder];
701   return YES;
702 }
703
704 # endif // !USE_PICKER_VIEW
705
706 #endif // USE_IPHONE
707
708
709 # ifndef USE_IPHONE
710
711 - (void) okAction:(NSObject *)arg
712 {
713   // Without the setAppliesImmediately:, when the saver restarts, it's still
714   // got the old settings. -[XScreenSaverConfigSheet traverseTree] sets this
715   // to NO; default is YES.
716
717   // #### However: I'm told that when these are set to YES, then changes to
718   // 'textLiteral', 'textURL' and 'textProgram' are ignored, but 'textFile'
719   // works.  In StarWars, at least...
720
721   [userDefaultsController   setAppliesImmediately:YES];
722   [globalDefaultsController setAppliesImmediately:YES];
723   [userDefaultsController   commitEditing];
724   [globalDefaultsController commitEditing];
725   [userDefaultsController   save:self];
726   [globalDefaultsController save:self];
727   [NSApp endSheet:self returnCode:NSOKButton];
728   [self close];
729 }
730
731 - (void) cancelAction:(NSObject *)arg
732 {
733   [userDefaultsController   revert:self];
734   [globalDefaultsController revert:self];
735   [NSApp endSheet:self returnCode:NSCancelButton];
736   [self close];
737 }
738 # endif // !USE_IPHONE
739
740
741 - (void) resetAction:(NSObject *)arg
742 {
743 # ifndef USE_IPHONE
744   [userDefaultsController   revertToInitialValues:self];
745   [globalDefaultsController revertToInitialValues:self];
746 # else  // USE_IPHONE
747
748   for (NSString *key in defaultOptions) {
749     NSObject *val = [defaultOptions objectForKey:key];
750     [[self controllerForKey:key] setObject:val forKey:key];
751   }
752
753   for (UIControl *ctl in pref_ctls) {
754     NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
755     [self bindResource:ctl key:pref_key reload:YES];
756   }
757
758   [self refreshTableView];
759 # endif // USE_IPHONE
760 }
761
762
763 /* Connects a control (checkbox, etc) to the corresponding preferences key.
764  */
765 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
766          reload:(BOOL)reload_p
767 {
768   NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
769 # ifndef USE_IPHONE
770   NSDictionary *opts_dict = nil;
771   NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
772                       ? @"selectedObject"
773                       : ([control isKindOfClass:[NSMatrix class]]
774                          ? @"selectedIndex"
775                          : @"value"));
776
777   if ([control isKindOfClass:[NSMatrix class]]) {
778     opts_dict = @{ NSValueTransformerNameBindingOption:
779                    @"TextModeTransformer" };
780   }
781
782   [control bind:bindto
783        toObject:prefs
784     withKeyPath:[@"values." stringByAppendingString: pref_key]
785         options:opts_dict];
786
787 # else  // USE_IPHONE
788   SEL sel;
789   NSObject *val = [prefs objectForKey:pref_key];
790   NSString *sval = 0;
791   double dval = 0;
792
793   if ([val isKindOfClass:[NSString class]]) {
794     sval = (NSString *) val;
795     if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
796         NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
797         NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
798       dval = 1;
799     else
800       dval = [sval doubleValue];
801   } else if ([val isKindOfClass:[NSNumber class]]) {
802     // NSBoolean (__NSCFBoolean) is really NSNumber.
803     dval = [(NSNumber *) val doubleValue];
804     sval = [(NSNumber *) val stringValue];
805   }
806
807   if ([control isKindOfClass:[UISlider class]]) {
808     sel = @selector(sliderAction:);
809     // Hacky API. See comment in InvertedSlider.m.
810     if ([control isKindOfClass:[InvertedSlider class]])
811       [(InvertedSlider *) control setTransformedValue: dval];
812     else
813       [(UISlider *) control setValue: dval];
814   } else if ([control isKindOfClass:[UISwitch class]]) {
815     sel = @selector(switchAction:);
816     [(UISwitch *) control setOn: ((int) dval != 0)];
817 # ifdef USE_PICKER_VIEW
818   } else if ([control isKindOfClass:[UIPickerView class]]) {
819     sel = 0;
820     [(UIPickerView *) control selectRow:((int)dval) inComponent:0
821                       animated:NO];
822 # else  // !USE_PICKER_VIEW
823   } else if ([control isKindOfClass:[RadioButton class]]) {
824     sel = 0;  // radioAction: sent from didSelectRowAtIndexPath.
825   } else if ([control isKindOfClass:[UITextField class]]) {
826     sel = 0;  // ####
827     [(UITextField *) control setText: sval];
828 # endif // !USE_PICKER_VIEW
829   } else {
830     NSAssert (0, @"unknown class");
831   }
832
833   // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
834
835   if (!reload_p) {
836     if (! pref_keys) {
837       pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
838       pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
839     }
840
841     [pref_keys addObject: [self makeKey:pref_key]];
842     [pref_ctls addObject: control];
843     ((UIControl *) control).tag = [pref_keys count] - 1;
844
845     if (sel) {
846       [(UIControl *) control addTarget:self action:sel
847                      forControlEvents:UIControlEventValueChanged];
848     }
849   }
850
851 # endif // USE_IPHONE
852
853 # if 0
854   NSObject *def = [[prefs defaults] objectForKey:pref_key];
855   NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
856   s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
857   s = [NSString stringWithFormat:@"%@ = %@", s, 
858                 ([def isKindOfClass:[NSString class]]
859                  ? [NSString stringWithFormat:@"\"%@\"", def]
860                  : def)];
861   s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0];
862   s = [NSString stringWithFormat:@"%@ %@ / %@", s,
863                 [def class], [control class]];
864 #  ifndef USE_IPHONE
865   s = [NSString stringWithFormat:@"%@ / %@", s, bindto];
866 #  endif
867   NSLog (@"%@", s);
868 # endif
869 }
870
871
872 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
873 {
874   [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
875 }
876
877
878
879 - (void) bindSwitch:(NSObject *)control
880             cmdline:(NSString *)cmd
881 {
882   [self bindResource:control 
883         key:[self switchToResource:cmd opts:opts valRet:0]];
884 }
885
886
887 #pragma mark Text-manipulating utilities
888
889
890 static NSString *
891 unwrap (NSString *text)
892 {
893   // Unwrap lines: delete \n but do not delete \n\n.
894   //
895   NSArray *lines = [text componentsSeparatedByString:@"\n"];
896   NSUInteger i, nlines = [lines count];
897   BOOL eolp = YES;
898
899   text = @"\n";      // start with one blank line
900
901   // skip trailing blank lines in file
902   for (i = nlines-1; i > 0; i--) {
903     NSString *s = (NSString *) [lines objectAtIndex:i];
904     if ([s length] > 0)
905       break;
906     nlines--;
907   }
908
909   // skip leading blank lines in file
910   for (i = 0; i < nlines; i++) {
911     NSString *s = (NSString *) [lines objectAtIndex:i];
912     if ([s length] > 0)
913       break;
914   }
915
916   // unwrap
917   Bool any = NO;
918   for (; i < nlines; i++) {
919     NSString *s = (NSString *) [lines objectAtIndex:i];
920     if ([s length] == 0) {
921       text = [text stringByAppendingString:@"\n\n"];
922       eolp = YES;
923     } else if ([s characterAtIndex:0] == ' ' ||
924                [s hasPrefix:@"Copyright "] ||
925                [s hasPrefix:@"https://"] ||
926                [s hasPrefix:@"http://"]) {
927       // don't unwrap if the following line begins with whitespace,
928       // or with the word "Copyright", or if it begins with a URL.
929       if (any && !eolp)
930         text = [text stringByAppendingString:@"\n"];
931       text = [text stringByAppendingString:s];
932       any = YES;
933       eolp = NO;
934     } else {
935       if (!eolp)
936         text = [text stringByAppendingString:@" "];
937       text = [text stringByAppendingString:s];
938       eolp = NO;
939       any = YES;
940     }
941   }
942
943   return text;
944 }
945
946
947 # ifndef USE_IPHONE
948 /* Makes the text up to the first comma be bold.
949  */
950 static void
951 boldify (NSText *nstext)
952 {
953   NSString *text = [nstext string];
954   NSRange r = [text rangeOfString:@"," options:0];
955   r.length = r.location+1;
956
957   r.location = 0;
958
959   NSFont *font = [nstext font];
960   font = [NSFont boldSystemFontOfSize:[font pointSize]];
961   [nstext setFont:font range:r];
962 }
963 # endif // !USE_IPHONE
964
965
966 /* Creates a human-readable anchor to put on a URL.
967  */
968 static char *
969 anchorize (const char *url)
970 {
971   const char *wiki1 =  "http://en.wikipedia.org/wiki/";
972   const char *wiki2 = "https://en.wikipedia.org/wiki/";
973   const char *math1 =  "http://mathworld.wolfram.com/";
974   const char *math2 = "https://mathworld.wolfram.com/";
975   if (!strncmp (wiki1, url, strlen(wiki1)) ||
976       !strncmp (wiki2, url, strlen(wiki2))) {
977     char *anchor = (char *) malloc (strlen(url) * 3 + 10);
978     strcpy (anchor, "Wikipedia: \"");
979     const char *in = url + strlen(!strncmp (wiki1, url, strlen(wiki1))
980                                   ? wiki1 : wiki2);
981     char *out = anchor + strlen(anchor);
982     while (*in) {
983       if (*in == '_') {
984         *out++ = ' ';
985       } else if (*in == '#') {
986         *out++ = ':';
987         *out++ = ' ';
988       } else if (*in == '%') {
989         char hex[3];
990         hex[0] = in[1];
991         hex[1] = in[2];
992         hex[2] = 0;
993         int n = 0;
994         sscanf (hex, "%x", &n);
995         *out++ = (char) n;
996         in += 2;
997       } else {
998         *out++ = *in;
999       }
1000       in++;
1001     }
1002     *out++ = '"';
1003     *out = 0;
1004     return anchor;
1005
1006   } else if (!strncmp (math1, url, strlen(math1)) ||
1007              !strncmp (math2, url, strlen(math2))) {
1008     char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1009     strcpy (anchor, "MathWorld: \"");
1010     const char *start = url + strlen(!strncmp (math1, url, strlen(math1))
1011                                      ? math1 : math2);
1012     const char *in = start;
1013     char *out = anchor + strlen(anchor);
1014     while (*in) {
1015       if (*in == '_') {
1016         *out++ = ' ';
1017       } else if (in != start && *in >= 'A' && *in <= 'Z') {
1018         *out++ = ' ';
1019         *out++ = *in;
1020       } else if (!strncmp (in, ".htm", 4)) {
1021         break;
1022       } else {
1023         *out++ = *in;
1024       }
1025       in++;
1026     }
1027     *out++ = '"';
1028     *out = 0;
1029     return anchor;
1030
1031   } else {
1032     return strdup (url);
1033   }
1034 }
1035
1036
1037 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
1038
1039 /* Converts any http: URLs in the given text field to clickable links.
1040  */
1041 static void
1042 hreffify (NSText *nstext)
1043 {
1044 # ifndef USE_IPHONE
1045   NSString *text = [nstext string];
1046   [nstext setRichText:YES];
1047 # else
1048   NSString *text = [nstext text];
1049 # endif
1050
1051   NSUInteger L = [text length];
1052   NSRange start;                // range is start-of-search to end-of-string
1053   start.location = 0;
1054   start.length = L;
1055   while (start.location < L) {
1056
1057     // Find the beginning of a URL...
1058     //
1059     NSRange r2 = [text rangeOfString: @"http://" options:0 range:start];
1060     NSRange r3 = [text rangeOfString:@"https://" options:0 range:start];
1061     if ((r2.location == NSNotFound &&
1062          r3.location != NSNotFound) ||
1063         (r2.location != NSNotFound &&
1064          r3.location != NSNotFound &&
1065          r3.location < r2.location))
1066       r2 = r3;
1067     if (r2.location == NSNotFound)
1068       break;
1069
1070     // Next time around, start searching after this.
1071     start.location = r2.location + r2.length;
1072     start.length = L - start.location;
1073
1074     // Find the end of a URL (whitespace or EOF)...
1075     //
1076     r3 = [text rangeOfCharacterFromSet:
1077                  [NSCharacterSet whitespaceAndNewlineCharacterSet]
1078                options:0 range:start];
1079     if (r3.location == NSNotFound)    // EOF
1080       r3.location = L, r3.length = 0;
1081
1082     // Next time around, start searching after this.
1083     start.location = r3.location;
1084     start.length = L - start.location;
1085
1086     // Set r2 to the start/length of this URL.
1087     r2.length = start.location - r2.location;
1088
1089     // Extract the URL.
1090     NSString *nsurl = [text substringWithRange:r2];
1091     const char *url = [nsurl UTF8String];
1092
1093     // If this is a Wikipedia URL, make the linked text be prettier.
1094     //
1095     char *anchor = anchorize(url);
1096
1097 # ifndef USE_IPHONE
1098
1099     // Construct the RTF corresponding to <A HREF="url">anchor</A>
1100     //
1101     const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1102     char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1103     sprintf (rtf, fmt, url, anchor);
1104
1105     NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1106     [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1107
1108 # else  // !USE_IPHONE
1109     // *anchor = 0; // Omit Wikipedia anchor 
1110     text = [text stringByReplacingCharactersInRange:r2
1111                  withString:[NSString stringWithCString:anchor
1112                                       encoding:NSUTF8StringEncoding]];
1113     // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1114     //              withString:@"\n\n"];
1115 # endif // !USE_IPHONE
1116
1117     free (anchor);
1118
1119     NSUInteger L2 = [text length];  // might have changed
1120     start.location -= (L - L2);
1121     L = L2;
1122   }
1123
1124 # ifdef USE_IPHONE
1125   [nstext setText:text];
1126   [nstext sizeToFit];
1127 # endif
1128 }
1129
1130 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1131
1132
1133
1134 #pragma mark Creating controls from XML
1135
1136
1137 /* Parse the attributes of an XML tag into a dictionary.
1138    For input, the dictionary should have as attributes the keys, each
1139    with @"" as their value.
1140    On output, the dictionary will set the keys to the values specified,
1141    and keys that were not specified will not be present in the dictionary.
1142    Warnings are printed if there are duplicate or unknown attributes.
1143  */
1144 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1145 {
1146   NSArray *attrs = [(NSXMLElement *) node attributes];
1147   NSUInteger n = [attrs count];
1148   int i;
1149   
1150   // For each key in the dictionary, fill in the dict with the corresponding
1151   // value.  The value @"" is assumed to mean "un-set".  Issue a warning if
1152   // an attribute is specified twice.
1153   //
1154   for (i = 0; i < n; i++) {
1155     NSXMLNode *attr = [attrs objectAtIndex:i];
1156     NSString *key = [attr name];
1157     NSString *val = [attr objectValue];
1158     NSString *old = [dict objectForKey:key];
1159     
1160     if (! old) {
1161       NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1162     } else if ([old length] != 0) {
1163       NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1164     } else {
1165       [dict setValue:val forKey:key];
1166     }
1167   }
1168   
1169   // Remove from the dictionary any keys whose value is still @"", 
1170   // meaning there was no such attribute specified.
1171   //
1172   NSArray *keys = [dict allKeys];
1173   n = [keys count];
1174   for (i = 0; i < n; i++) {
1175     NSString *key = [keys objectAtIndex:i];
1176     NSString *val = [dict objectForKey:key];
1177     if ([val length] == 0)
1178       [dict removeObjectForKey:key];
1179   }
1180
1181 # ifdef USE_IPHONE
1182   // Kludge for starwars.xml:
1183   // If there is a "_low-label" and no "_label", but "_low-label" contains
1184   // spaces, divide them.
1185   NSString *lab = [dict objectForKey:@"_label"];
1186   NSString *low = [dict objectForKey:@"_low-label"];
1187   if (low && !lab) {
1188     NSArray *split =
1189       [[[low stringByTrimmingCharactersInSet:
1190                [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1191          componentsSeparatedByString: @"  "]
1192         filteredArrayUsingPredicate:
1193           [NSPredicate predicateWithFormat:@"length > 0"]];
1194     if (split && [split count] == 2) {
1195       [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1196       [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1197     }
1198   }
1199 # endif // USE_IPHONE
1200 }
1201
1202
1203 /* Handle the options on the top level <xscreensaver> tag.
1204  */
1205 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1206 {
1207   NSMutableDictionary *dict = [@{ @"name":   @"",
1208                                   @"_label": @"",
1209                                   @"gl":     @"" }
1210                                 mutableCopy];
1211   [self parseAttrs:dict node:node];
1212   NSString *name  = [dict objectForKey:@"name"];
1213   NSString *label = [dict objectForKey:@"_label"];
1214   [dict release];
1215   dict = 0;
1216     
1217   NSAssert1 (label, @"no _label in %@", [node name]);
1218   NSAssert1 (name, @"no name in \"%@\"", label);
1219   return label;
1220 }
1221
1222
1223 /* Creates a label: an un-editable NSTextField displaying the given text.
1224  */
1225 - (LABEL *) makeLabel:(NSString *)text
1226 {
1227   NSRect rect;
1228   rect.origin.x = rect.origin.y = 0;
1229   rect.size.width = rect.size.height = 10;
1230 # ifndef USE_IPHONE
1231   NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1232   [lab setSelectable:NO];
1233   [lab setEditable:NO];
1234   [lab setBezeled:NO];
1235   [lab setDrawsBackground:NO];
1236   [lab setStringValue:text];
1237   [lab sizeToFit];
1238 # else  // USE_IPHONE
1239   UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1240   [lab setText: [text stringByTrimmingCharactersInSet:
1241                  [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1242   [lab setBackgroundColor:[UIColor clearColor]];
1243   [lab setNumberOfLines:0]; // unlimited
1244   // [lab setLineBreakMode:UILineBreakModeWordWrap];
1245   [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1246   [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1247                              UIViewAutoresizingFlexibleHeight)];
1248 # endif // USE_IPHONE
1249   [lab autorelease];
1250   return lab;
1251 }
1252
1253
1254 /* Creates the checkbox (NSButton) described by the given XML node.
1255  */
1256 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1257 {
1258   NSMutableDictionary *dict = [@{ @"id":       @"",
1259                                   @"_label":    @"",
1260                                   @"arg-set":   @"",
1261                                   @"arg-unset": @"" }
1262                                 mutableCopy];
1263   [self parseAttrs:dict node:node];
1264   NSString *label     = [dict objectForKey:@"_label"];
1265   NSString *arg_set   = [dict objectForKey:@"arg-set"];
1266   NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1267   [dict release];
1268   dict = 0;
1269   
1270   if (!label) {
1271     NSAssert1 (0, @"no _label in %@", [node name]);
1272     return;
1273   }
1274   if (!arg_set && !arg_unset) {
1275     NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"", 
1276                label);
1277   }
1278   if (arg_set && arg_unset) {
1279     NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"", 
1280                label);
1281   }
1282   
1283   // sanity-check the choice of argument names.
1284   //
1285   if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1286                   [arg_set hasPrefix:@"--no-"]))
1287     NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1288            label, arg_set);
1289   if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1290                     ![arg_unset hasPrefix:@"--no-"]))
1291     NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1292           label, arg_unset);
1293     
1294   NSRect rect;
1295   rect.origin.x = rect.origin.y = 0;
1296   rect.size.width = rect.size.height = 10;
1297
1298 # ifndef USE_IPHONE
1299
1300   NSButton *button = [[NSButton alloc] initWithFrame:rect];
1301   [button setButtonType:NSSwitchButton];
1302   [button setTitle:label];
1303   [button sizeToFit];
1304   [self placeChild:button on:parent];
1305
1306 # else  // USE_IPHONE
1307
1308   LABEL *lab = [self makeLabel:label];
1309   [self placeChild:lab on:parent];
1310   UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1311   [self placeChild:button on:parent right:YES];
1312
1313 # endif // USE_IPHONE
1314   
1315   [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1316   [button release];
1317 }
1318
1319
1320 /* Creates the number selection control described by the given XML node.
1321    If "type=slider", it's an NSSlider.
1322    If "type=spinbutton", it's a text field with up/down arrows next to it.
1323  */
1324 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1325 {
1326   NSMutableDictionary *dict = [@{ @"id":          @"",
1327                                   @"_label":      @"",
1328                                   @"_low-label":  @"",
1329                                   @"_high-label": @"",
1330                                   @"type":        @"",
1331                                   @"arg":         @"",
1332                                   @"low":         @"",
1333                                   @"high":        @"",
1334                                   @"default":     @"",
1335                                   @"convert":     @"" }
1336                                 mutableCopy];
1337   [self parseAttrs:dict node:node];
1338   NSString *label      = [dict objectForKey:@"_label"];
1339   NSString *low_label  = [dict objectForKey:@"_low-label"];
1340   NSString *high_label = [dict objectForKey:@"_high-label"];
1341   NSString *type       = [dict objectForKey:@"type"];
1342   NSString *arg        = [dict objectForKey:@"arg"];
1343   NSString *low        = [dict objectForKey:@"low"];
1344   NSString *high       = [dict objectForKey:@"high"];
1345   NSString *def        = [dict objectForKey:@"default"];
1346   NSString *cvt        = [dict objectForKey:@"convert"];
1347   [dict release];
1348   dict = 0;
1349   
1350   NSAssert1 (arg,  @"no arg in %@", label);
1351   NSAssert1 (type, @"no type in %@", label);
1352
1353   if (! low) {
1354     NSAssert1 (0, @"no low in %@", [node name]);
1355     return;
1356   }
1357   if (! high) {
1358     NSAssert1 (0, @"no high in %@", [node name]);
1359     return;
1360   }
1361   if (! def) {
1362     NSAssert1 (0, @"no default in %@", [node name]);
1363     return;
1364   }
1365   if (cvt && ![cvt isEqualToString:@"invert"]) {
1366     NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1367                label);
1368   }
1369     
1370   // If either the min or max field contains a decimal point, then this
1371   // option may have a floating point value; otherwise, it is constrained
1372   // to be an integer.
1373   //
1374   NSCharacterSet *dot =
1375     [NSCharacterSet characterSetWithCharactersInString:@"."];
1376   BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1377                   [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1378
1379   if ([type isEqualToString:@"slider"]
1380 # ifdef USE_IPHONE  // On iPhone, we use sliders for all numeric values.
1381       || [type isEqualToString:@"spinbutton"]
1382 # endif
1383       ) {
1384
1385     NSRect rect;
1386     rect.origin.x = rect.origin.y = 0;    
1387     rect.size.width = 150;
1388     rect.size.height = 23;  // apparent min height for slider with ticks...
1389     NSSlider *slider;
1390     slider = [[InvertedSlider alloc] initWithFrame:rect
1391                                      inverted: !!cvt
1392                                      integers: !float_p];
1393     [slider setMaxValue:[high doubleValue]];
1394     [slider setMinValue:[low  doubleValue]];
1395     
1396     int range = [slider maxValue] - [slider minValue] + 1;
1397     int range2 = range;
1398     int max_ticks = 21;
1399     while (range2 > max_ticks)
1400       range2 /= 10;
1401
1402 # ifndef USE_IPHONE
1403     // If we have elided ticks, leave it at the max number of ticks.
1404     if (range != range2 && range2 < max_ticks)
1405       range2 = max_ticks;
1406
1407     // If it's a float, always display the max number of ticks.
1408     if (float_p && range2 < max_ticks)
1409       range2 = max_ticks;
1410
1411     [slider setNumberOfTickMarks:range2];
1412
1413     [slider setAllowsTickMarkValuesOnly:
1414               (range == range2 &&  // we are showing the actual number of ticks
1415                !float_p)];         // and we want integer results
1416 # endif // !USE_IPHONE
1417
1418     // #### Note: when the slider's range is large enough that we aren't
1419     //      showing all possible ticks, the slider's value is not constrained
1420     //      to be an integer, even though it should be...
1421     //      Maybe we need to use a value converter or something?
1422
1423     LABEL *lab;
1424     if (label) {
1425       lab = [self makeLabel:label];
1426       [self placeChild:lab on:parent];
1427 # ifdef USE_IPHONE
1428       if (low_label) {
1429         CGFloat s = [NSFont systemFontSize] + 4;
1430         [lab setFont:[NSFont boldSystemFontOfSize:s]];
1431       }
1432 # endif
1433     }
1434     
1435     if (low_label) {
1436       lab = [self makeLabel:low_label];
1437       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
1438 # ifndef USE_IPHONE
1439       [lab setAlignment:1];  // right aligned
1440       rect = [lab frame];
1441       if (rect.size.width < LEFT_LABEL_WIDTH)
1442         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
1443       rect.size.height = [slider frame].size.height;
1444       [lab setFrame:rect];
1445       [self placeChild:lab on:parent];
1446 # else  // USE_IPHONE
1447       [lab setTextAlignment: NSTextAlignmentRight];
1448       // Sometimes rotation screws up truncation.
1449       [lab setLineBreakMode:NSLineBreakByClipping];
1450       [self placeChild:lab on:parent right:(label ? YES : NO)];
1451 # endif // USE_IPHONE
1452      }
1453     
1454 # ifndef USE_IPHONE
1455     [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1456 # else  // USE_IPHONE
1457     [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1458 # endif // USE_IPHONE
1459     
1460     if (low_label) {
1461       // Make left label be same height as slider.
1462       rect = [lab frame];
1463       rect.size.height = [slider frame].size.height;
1464       [lab setFrame:rect];
1465     }
1466
1467     if (! low_label) {
1468       rect = [slider frame];
1469       if (rect.origin.x < LEFT_LABEL_WIDTH)
1470         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1471       [slider setFrame:rect];
1472     }
1473         
1474     if (high_label) {
1475       lab = [self makeLabel:high_label];
1476       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1477       rect = [lab frame];
1478
1479       // Make right label be same height as slider.
1480       rect.size.height = [slider frame].size.height;
1481       [lab setFrame:rect];
1482 # ifdef USE_IPHONE
1483       // Sometimes rotation screws up truncation.
1484       [lab setLineBreakMode:NSLineBreakByClipping];
1485 # endif
1486       [self placeChild:lab on:parent right:YES];
1487      }
1488
1489     [self bindSwitch:slider cmdline:arg];
1490     [slider release];
1491     
1492 #ifndef USE_IPHONE  // On iPhone, we use sliders for all numeric values.
1493
1494   } else if ([type isEqualToString:@"spinbutton"]) {
1495
1496     if (! label) {
1497       NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1498       return;
1499     }
1500     NSAssert1 (!low_label,
1501               @"low-label not allowed in spinbutton \"%@\"", [node name]);
1502     NSAssert1 (!high_label,
1503                @"high-label not allowed in spinbutton \"%@\"", [node name]);
1504     NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1505                [node name]);
1506     
1507     NSRect rect;
1508     rect.origin.x = rect.origin.y = 0;    
1509     rect.size.width = rect.size.height = 10;
1510     
1511     NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1512     [txt setStringValue:@"0000.0"];
1513     [txt sizeToFit];
1514     [txt setStringValue:@""];
1515     
1516     if (label) {
1517       LABEL *lab = [self makeLabel:label];
1518       //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1519       [lab setAlignment:1];  // right aligned
1520       rect = [lab frame];
1521       if (rect.size.width < LEFT_LABEL_WIDTH)
1522         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
1523       rect.size.height = [txt frame].size.height;
1524       [lab setFrame:rect];
1525       [self placeChild:lab on:parent];
1526      }
1527     
1528     [self placeChild:txt on:parent right:(label ? YES : NO)];
1529     
1530     if (! label) {
1531       rect = [txt frame];
1532       if (rect.origin.x < LEFT_LABEL_WIDTH)
1533         rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1534       [txt setFrame:rect];
1535     }
1536     
1537     rect.size.width = rect.size.height = 10;
1538     NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1539     [step sizeToFit];
1540     [self placeChild:step on:parent right:YES];
1541     rect = [step frame];
1542     rect.origin.x -= COLUMN_SPACING;  // this one goes close
1543     rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1544     [step setFrame:rect];
1545     
1546     [step setMinValue:[low  doubleValue]];
1547     [step setMaxValue:[high doubleValue]];
1548     [step setAutorepeat:YES];
1549     [step setValueWraps:NO];
1550     
1551     double range = [high doubleValue] - [low doubleValue];
1552     if (range < 1.0)
1553       [step setIncrement:range / 10.0];
1554     else if (range >= 500)
1555       [step setIncrement:range / 100.0];
1556     else
1557       [step setIncrement:1.0];
1558
1559     NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1560     [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1561     [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1562     [fmt setMinimum:[NSNumber numberWithDouble:[low  doubleValue]]];
1563     [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1564     [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1565     [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1566
1567     [fmt setGeneratesDecimalNumbers:float_p];
1568     [[txt cell] setFormatter:fmt];
1569
1570     [self bindSwitch:step cmdline:arg];
1571     [self bindSwitch:txt  cmdline:arg];
1572     
1573     [step release];
1574     [txt release];
1575
1576 # endif // USE_IPHONE
1577
1578   } else {
1579     NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1580   }
1581 }
1582
1583
1584 # ifndef USE_IPHONE
1585 static void
1586 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1587 {
1588   /* If the object associated with this menu item looks like a boolean,
1589      store an NSNumber instead of an NSString, since that's what
1590      will be in the preferences (due to similar logic in PrefsReader).
1591    */
1592   if ([obj isKindOfClass:[NSString class]]) {
1593     NSString *string = (NSString *) obj;
1594     if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1595         NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1596       obj = [NSNumber numberWithBool:YES];
1597     else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1598              NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1599       obj = [NSNumber numberWithBool:NO];
1600     else
1601       obj = string;
1602   }
1603
1604   [item setRepresentedObject:obj];
1605   //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1606 }
1607 # endif // !USE_IPHONE
1608
1609
1610 /* Creates the popup menu described by the given XML node (and its children).
1611  */
1612 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1613 {
1614   NSArray *children = [node children];
1615   NSUInteger i, count = [children count];
1616
1617   if (count <= 0) {
1618     NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1619     return;
1620   }
1621
1622   // get the "id" attribute off the <select> tag.
1623   //
1624   NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1625   [self parseAttrs:dict node:node];
1626   [dict release];
1627   dict = 0;
1628   
1629   NSRect rect;
1630   rect.origin.x = rect.origin.y = 0;
1631   rect.size.width = 10;
1632   rect.size.height = 10;
1633
1634   NSString *menu_key = nil;   // the resource key used by items in this menu
1635
1636 # ifndef USE_IPHONE
1637   // #### "Build and Analyze" says that all of our widgets leak, because it
1638   //      seems to not realize that placeChild -> addSubview retains them.
1639   //      Not sure what to do to make these warnings go away.
1640
1641   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1642                                                      pullsDown:NO];
1643   NSMenuItem *def_item = nil;
1644   float max_width = 0;
1645
1646 # else  // USE_IPHONE
1647
1648   NSString *def_item = nil;
1649
1650   rect.size.width  = 0;
1651   rect.size.height = 0;
1652 #  ifdef USE_PICKER_VIEW
1653   UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1654   popup.delegate = self;
1655   popup.dataSource = self;
1656 #  endif // !USE_PICKER_VIEW
1657   NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1658
1659 # endif // USE_IPHONE
1660   
1661   for (i = 0; i < count; i++) {
1662     NSXMLNode *child = [children objectAtIndex:i];
1663
1664     if ([child kind] == NSXMLCommentKind)
1665       continue;
1666     if ([child kind] != NSXMLElementKind) {
1667 //    NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1668       continue;
1669     }
1670
1671     // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1672     //
1673     NSMutableDictionary *dict2 = [@{ @"id":      @"",
1674                                      @"_label":  @"",
1675                                      @"arg-set": @"" }
1676                                    mutableCopy];
1677     [self parseAttrs:dict2 node:child];
1678     NSString *label   = [dict2 objectForKey:@"_label"];
1679     NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1680     [dict2 release];
1681     dict2 = 0;
1682     
1683     if (!label) {
1684       NSAssert1 (0, @"no _label in %@", [child name]);
1685       continue;
1686     }
1687
1688 # ifndef USE_IPHONE
1689     // create the menu item (and then get a pointer to it)
1690     [popup addItemWithTitle:label];
1691     NSMenuItem *item = [popup itemWithTitle:label];
1692 # endif // USE_IPHONE
1693
1694     if (arg_set) {
1695       NSString *this_val = NULL;
1696       NSString *this_key = [self switchToResource: arg_set
1697                                  opts: opts
1698                                  valRet: &this_val];
1699       NSAssert1 (this_val, @"this_val null for %@", arg_set);
1700       if (menu_key && ![menu_key isEqualToString:this_key])
1701         NSAssert3 (0,
1702                    @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1703                    menu_key, this_key, this_val);
1704       if (this_key)
1705         menu_key = this_key;
1706
1707       /* If this menu has the cmd line "-mode foo" then set this item's
1708          value to "foo" (the menu itself will be bound to e.g. "modeString")
1709        */
1710 # ifndef USE_IPHONE
1711       set_menu_item_object (item, this_val);
1712 # else
1713       // Array holds ["Label", "resource-key", "resource-val"].
1714       [items addObject:[NSMutableArray arrayWithObjects:
1715                                          label, @"", this_val, nil]];
1716 # endif
1717
1718     } else {
1719       // no arg-set -- only one menu item can be missing that.
1720       NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1721 # ifndef USE_IPHONE
1722       def_item = item;
1723 # else
1724       def_item = label;
1725       // Array holds ["Label", "resource-key", "resource-val"].
1726       [items addObject:[NSMutableArray arrayWithObjects:
1727                                          label, @"", @"", nil]];
1728 # endif
1729     }
1730
1731     /* make sure the menu button has room for the text of this item,
1732        and remember the greatest width it has reached.
1733      */
1734 # ifndef USE_IPHONE
1735     [popup setTitle:label];
1736     [popup sizeToFit];
1737     NSRect r = [popup frame];
1738     if (r.size.width > max_width) max_width = r.size.width;
1739 # endif // USE_IPHONE
1740   }
1741   
1742   if (!menu_key) {
1743     NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1744     return;
1745   }
1746
1747   /* We've added all of the menu items.  If there was an item with no
1748      command-line switch, then it's the item that represents the default
1749      value.  Now we must bind to that item as well...  (We have to bind
1750      this one late, because if it was the first item, then we didn't
1751      yet know what resource was associated with this menu.)
1752    */
1753   if (def_item) {
1754     NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1755     NSAssert2 (def_obj, 
1756                @"no default value for resource \"%@\" in menu item \"%@\"",
1757                menu_key,
1758 # ifndef USE_IPHONE
1759                [def_item title]
1760 # else
1761                def_item
1762 # endif
1763                );
1764
1765 # ifndef USE_IPHONE
1766     set_menu_item_object (def_item, def_obj);
1767 # else  // !USE_IPHONE
1768     for (NSMutableArray *a in items) {
1769       // Make sure each array contains the resource key.
1770       [a replaceObjectAtIndex:1 withObject:menu_key];
1771       // Make sure the default item contains the default resource value.
1772       if (def_obj && def_item &&
1773           [def_item isEqualToString:[a objectAtIndex:0]])
1774         [a replaceObjectAtIndex:2 withObject:def_obj];
1775     }
1776 # endif // !USE_IPHONE
1777   }
1778
1779 # ifndef USE_IPHONE
1780 #  ifdef USE_PICKER_VIEW
1781   /* Finish tweaking the menu button itself.
1782    */
1783   if (def_item)
1784     [popup setTitle:[def_item title]];
1785   NSRect r = [popup frame];
1786   r.size.width = max_width;
1787   [popup setFrame:r];
1788 #  endif // USE_PICKER_VIEW
1789 # endif
1790
1791 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1792   [self placeChild:popup on:parent];
1793   [self bindResource:popup key:menu_key];
1794   [popup release];
1795 # endif
1796
1797 # ifdef USE_IPHONE
1798 #  ifdef USE_PICKER_VIEW
1799   // Store the items for this picker in the picker_values array.
1800   // This is so fucking stupid.
1801
1802   unsigned long menu_number = [pref_keys count] - 1;
1803   if (! picker_values)
1804     picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1805   while ([picker_values count] <= menu_number)
1806     [picker_values addObject:[NSArray arrayWithObjects: nil]];
1807   [picker_values replaceObjectAtIndex:menu_number withObject:items];
1808   [popup reloadAllComponents];
1809
1810 #  else  // !USE_PICKER_VIEW
1811
1812   [self placeSeparator];
1813
1814   i = 0;
1815   for (__attribute__((unused)) NSArray *item in items) {
1816     RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1817                                           items:items];
1818     [b setLineBreakMode:NSLineBreakByTruncatingHead];
1819     [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1820     [self placeChild:b on:parent];
1821     [b release];
1822     i++;
1823   }
1824
1825   [self placeSeparator];
1826
1827 #  endif // !USE_PICKER_VIEW
1828 # endif // !USE_IPHONE
1829
1830 }
1831
1832
1833 /* Creates an uneditable, wrapping NSTextField to display the given
1834    text enclosed by <description> ... </description> in the XML.
1835  */
1836 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1837 {
1838   NSString *text = nil;
1839   NSArray *children = [node children];
1840   NSUInteger i, count = [children count];
1841
1842   for (i = 0; i < count; i++) {
1843     NSXMLNode *child = [children objectAtIndex:i];
1844     NSString *s = [child objectValue];
1845     if (text)
1846       text = [text stringByAppendingString:s];
1847     else
1848       text = s;
1849   }
1850   
1851   text = unwrap (text);
1852   
1853   NSRect rect = [parent frame];
1854   rect.origin.x = rect.origin.y = 0;
1855   rect.size.width = 200;
1856   rect.size.height = 50;  // sized later
1857 # ifndef USE_IPHONE
1858   NSText *lab = [[NSText alloc] initWithFrame:rect];
1859   [lab autorelease];
1860   [lab setEditable:NO];
1861   [lab setDrawsBackground:NO];
1862   [lab setHorizontallyResizable:YES];
1863   [lab setVerticallyResizable:YES];
1864   [lab setString:text];
1865   hreffify (lab);
1866   boldify (lab);
1867   [lab sizeToFit];
1868
1869 # else  // USE_IPHONE
1870
1871 #  ifndef USE_HTML_LABELS
1872
1873   UILabel *lab = [self makeLabel:text];
1874   [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1875   hreffify (lab);
1876
1877 #  else  // USE_HTML_LABELS
1878   HTMLLabel *lab = [[HTMLLabel alloc] 
1879                      initWithText:text
1880                      font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1881   [lab autorelease];
1882   [lab setFrame:rect];
1883   [lab sizeToFit];
1884 #  endif // USE_HTML_LABELS
1885
1886   [self placeSeparator];
1887
1888 # endif // USE_IPHONE
1889
1890   [self placeChild:lab on:parent];
1891 }
1892
1893
1894 /* Creates the NSTextField described by the given XML node.
1895  */
1896 - (void) makeTextField: (NSXMLNode *)node
1897                     on: (NSView *)parent
1898              withLabel: (BOOL) label_p
1899             horizontal: (BOOL) horiz_p
1900 {
1901   NSMutableDictionary *dict = [@{ @"id":     @"",
1902                                   @"_label": @"",
1903                                   @"arg":    @"" }
1904                                 mutableCopy];
1905   [self parseAttrs:dict node:node];
1906   NSString *label = [dict objectForKey:@"_label"];
1907   NSString *arg   = [dict objectForKey:@"arg"];
1908   [dict release];
1909   dict = 0;
1910
1911   if (!label && label_p) {
1912     NSAssert1 (0, @"no _label in %@", [node name]);
1913     return;
1914   }
1915
1916   NSAssert1 (arg, @"no arg in %@", label);
1917
1918   NSRect rect;
1919   rect.origin.x = rect.origin.y = 0;    
1920   rect.size.width = rect.size.height = 10;
1921   
1922   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1923
1924 # ifndef USE_IPHONE
1925
1926   // make the default size be around 30 columns; a typical value for
1927   // these text fields is "xscreensaver-text --cols 40".
1928   //
1929   [txt setStringValue:@"123456789 123456789 123456789 "];
1930   [txt sizeToFit];
1931   [[txt cell] setWraps:NO];
1932   [[txt cell] setScrollable:YES];
1933   [txt setStringValue:@""];
1934   
1935 # else  // USE_IPHONE
1936
1937   txt.adjustsFontSizeToFitWidth = YES;
1938   txt.textColor = [UIColor blackColor];
1939   txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1940   txt.placeholder = @"";
1941   txt.borderStyle = UITextBorderStyleRoundedRect;
1942   txt.textAlignment = NSTextAlignmentRight;
1943   txt.keyboardType = UIKeyboardTypeDefault;  // Full kbd
1944   txt.autocorrectionType = UITextAutocorrectionTypeNo;
1945   txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1946   txt.clearButtonMode = UITextFieldViewModeAlways;
1947   txt.returnKeyType = UIReturnKeyDone;
1948   txt.delegate = self;
1949   txt.text = @"";
1950   [txt setEnabled: YES];
1951
1952   rect.size.height = [txt.font lineHeight] * 1.2;
1953   [txt setFrame:rect];
1954
1955 # endif // USE_IPHONE
1956
1957   if (label) {
1958     LABEL *lab = [self makeLabel:label];
1959     [self placeChild:lab on:parent];
1960   }
1961
1962   [self placeChild:txt on:parent right:(label ? YES : NO)];
1963
1964   [self bindSwitch:txt cmdline:arg];
1965   [txt release];
1966 }
1967
1968
1969 /* Creates the NSTextField described by the given XML node,
1970    and hooks it up to a Choose button and a file selector widget.
1971  */
1972 - (void) makeFileSelector: (NSXMLNode *)node
1973                        on: (NSView *)parent
1974                  dirsOnly: (BOOL) dirsOnly
1975                 withLabel: (BOOL) label_p
1976                  editable: (BOOL) editable_p
1977 {
1978 # ifndef USE_IPHONE     // No files. No selectors.
1979   NSMutableDictionary *dict = [@{ @"id":     @"",
1980                                   @"_label": @"",
1981                                   @"arg":    @"" }
1982                                 mutableCopy];
1983   [self parseAttrs:dict node:node];
1984   NSString *label = [dict objectForKey:@"_label"];
1985   NSString *arg   = [dict objectForKey:@"arg"];
1986   [dict release];
1987   dict = 0;
1988
1989   if (!label && label_p) {
1990     NSAssert1 (0, @"no _label in %@", [node name]);
1991     return;
1992   }
1993
1994   NSAssert1 (arg, @"no arg in %@", label);
1995
1996   NSRect rect;
1997   rect.origin.x = rect.origin.y = 0;    
1998   rect.size.width = rect.size.height = 10;
1999   
2000   NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
2001
2002   // make the default size be around 20 columns.
2003   //
2004   [txt setStringValue:@"123456789 123456789 "];
2005   [txt sizeToFit];
2006   [txt setSelectable:YES];
2007   [txt setEditable:editable_p];
2008   [txt setBezeled:editable_p];
2009   [txt setDrawsBackground:editable_p];
2010   [[txt cell] setWraps:NO];
2011   [[txt cell] setScrollable:YES];
2012   [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
2013   [txt setStringValue:@""];
2014
2015   LABEL *lab = 0;
2016   if (label) {
2017     lab = [self makeLabel:label];
2018     [self placeChild:lab on:parent];
2019   }
2020
2021   [self placeChild:txt on:parent right:(label ? YES : NO)];
2022
2023   [self bindSwitch:txt cmdline:arg];
2024   [txt release];
2025
2026   // Make the text field and label be the same height, whichever is taller.
2027   if (lab) {
2028     rect = [txt frame];
2029     rect.size.height = ([lab frame].size.height > [txt frame].size.height
2030                         ? [lab frame].size.height
2031                         : [txt frame].size.height);
2032     [txt setFrame:rect];
2033   }
2034
2035   // Now put a "Choose" button next to it.
2036   //
2037   rect.origin.x = rect.origin.y = 0;    
2038   rect.size.width = rect.size.height = 10;
2039   NSButton *choose = [[NSButton alloc] initWithFrame:rect];
2040   [choose setTitle:@"Choose..."];
2041   [choose setBezelStyle:NSRoundedBezelStyle];
2042   [choose sizeToFit];
2043
2044   [self placeChild:choose on:parent right:YES];
2045
2046   // center the Choose button around the midpoint of the text field.
2047   rect = [choose frame];
2048   rect.origin.y = ([txt frame].origin.y + 
2049                    (([txt frame].size.height - rect.size.height) / 2));
2050   [choose setFrameOrigin:rect.origin];
2051
2052   [choose setTarget:[parent window]];
2053   if (dirsOnly)
2054     [choose setAction:@selector(fileSelectorChooseDirsAction:)];
2055   else
2056     [choose setAction:@selector(fileSelectorChooseAction:)];
2057
2058   [choose release];
2059 # endif // !USE_IPHONE
2060 }
2061
2062
2063 # ifndef USE_IPHONE
2064
2065 /* Runs a modal file selector and sets the text field's value to the
2066    selected file or directory.
2067  */
2068 static void
2069 do_file_selector (NSTextField *txt, BOOL dirs_p)
2070 {
2071   NSOpenPanel *panel = [NSOpenPanel openPanel];
2072   [panel setAllowsMultipleSelection:NO];
2073   [panel setCanChooseFiles:!dirs_p];
2074   [panel setCanChooseDirectories:dirs_p];
2075
2076   NSInteger result = [panel runModal];
2077   if (result == NSOKButton) {
2078     NSArray *files = [panel URLs];
2079     NSString *file = ([files count] > 0 ? [[files objectAtIndex:0] path] : @"");
2080     file = [file stringByAbbreviatingWithTildeInPath];
2081     [txt setStringValue:file];
2082
2083     // Fuck me!  Just setting the value of the NSTextField does not cause
2084     // that to end up in the preferences!
2085     //
2086     NSDictionary *dict = [txt infoForBinding:@"value"];
2087     NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
2088     NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
2089     if ([path hasPrefix:@"values."])  // WTF.
2090       path = [path substringFromIndex:7];
2091     [[prefs values] setValue:file forKey:path];
2092   }
2093 }
2094
2095
2096 /* Returns the NSTextField that is to the left of or above the NSButton.
2097  */
2098 static NSTextField *
2099 find_text_field_of_button (NSButton *button)
2100 {
2101   NSView *parent = [button superview];
2102   NSArray *kids = [parent subviews];
2103   NSUInteger nkids = [kids count];
2104   int i;
2105   NSTextField *f = 0;
2106   for (i = 0; i < nkids; i++) {
2107     NSObject *kid = [kids objectAtIndex:i];
2108     if ([kid isKindOfClass:[NSTextField class]]) {
2109       f = (NSTextField *) kid;
2110     } else if (kid == button) {
2111       if (! f) abort();
2112       return f;
2113     }
2114   }
2115   abort();
2116 }
2117
2118
2119 - (void) fileSelectorChooseAction:(NSObject *)arg
2120 {
2121   NSButton *choose = (NSButton *) arg;
2122   NSTextField *txt = find_text_field_of_button (choose);
2123   do_file_selector (txt, NO);
2124 }
2125
2126 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2127 {
2128   NSButton *choose = (NSButton *) arg;
2129   NSTextField *txt = find_text_field_of_button (choose);
2130   do_file_selector (txt, YES);
2131 }
2132
2133 #endif // !USE_IPHONE
2134
2135
2136 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2137 {
2138 # ifndef USE_IPHONE
2139   /*
2140     Display Text:
2141      (x)  Computer name and time
2142      ( )  Text       [__________________________]
2143      ( )  Text file  [_________________] [Choose]
2144      ( )  URL        [__________________________]
2145      ( )  Shell Cmd  [__________________________]
2146
2147     textMode -text-mode date
2148     textMode -text-mode literal   textLiteral -text-literal %
2149     textMode -text-mode file      textFile    -text-file %
2150     textMode -text-mode url       textURL     -text-url %
2151     textMode -text-mode program   textProgram -text-program %
2152    */
2153   NSRect rect;
2154   rect.size.width = rect.size.height = 1;
2155   rect.origin.x = rect.origin.y = 0;
2156   NSView *group  = [[NSView alloc] initWithFrame:rect];
2157   NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2158
2159   Bool program_p = TRUE;
2160
2161
2162   NSView *control;
2163
2164   // This is how you link radio buttons together.
2165   //
2166   NSButtonCell *proto = [[NSButtonCell alloc] init];
2167   [proto setButtonType:NSRadioButton];
2168
2169   rect.origin.x = rect.origin.y = 0;
2170   rect.size.width = rect.size.height = 10;
2171   NSMatrix *matrix = [[NSMatrix alloc] 
2172                        initWithFrame:rect
2173                        mode:NSRadioModeMatrix
2174                        prototype:proto
2175                        numberOfRows: 4 + (program_p ? 1 : 0)
2176                        numberOfColumns:1];
2177   [matrix setAllowsEmptySelection:NO];
2178
2179   NSArrayController *cnames  = [[NSArrayController alloc] initWithContent:nil];
2180   [cnames addObject:@"Computer name and time"];
2181   [cnames addObject:@"Text"];
2182   [cnames addObject:@"File"];
2183   [cnames addObject:@"URL"];
2184   if (program_p) [cnames addObject:@"Shell Cmd"];
2185   [matrix bind:@"content"
2186           toObject:cnames
2187           withKeyPath:@"arrangedObjects"
2188           options:nil];
2189   [cnames release];
2190
2191   [self bindSwitch:matrix cmdline:@"-text-mode %"];
2192
2193   [self placeChild:matrix on:group];
2194   [self placeChild:rgroup on:group right:YES];
2195   [proto release];
2196   [matrix release];
2197   [rgroup release];
2198
2199   NSXMLNode *node2;
2200
2201 # else  // USE_IPHONE
2202
2203   NSView *rgroup = parent;
2204   NSXMLNode *node2;
2205
2206   // <select id="textMode">
2207   //   <option id="date"  _label="Display date" arg-set="-text-mode date"/>
2208   //   <option id="text"  _label="Display text" arg-set="-text-mode literal"/>
2209   //   <option id="url"   _label="Display URL"/>
2210   // </select>
2211
2212   node2 = [[NSXMLElement alloc] initWithName:@"select"];
2213   [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2214
2215   NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2216   [node3 setAttributesAsDictionary:
2217            @{ @"id":      @"date",
2218               @"arg-set": @"-text-mode date",
2219               @"_label":  @"Display the date and time" }];
2220   [node3 setParent: node2];
2221   [node3 autorelease];
2222
2223   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2224   [node3 setAttributesAsDictionary:
2225            @{ @"id":      @"text",
2226               @"arg-set": @"-text-mode literal",
2227               @"_label":  @"Display static text" }];
2228   [node3 setParent: node2];
2229   [node3 autorelease];
2230
2231   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2232   [node3 setAttributesAsDictionary:
2233            @{ @"id":     @"url",                           
2234               @"_label": @"Display the contents of a URL" }];
2235   [node3 setParent: node2];
2236   [node3 autorelease];
2237
2238   [self makeOptionMenu:node2 on:rgroup];
2239   [node2 release];
2240
2241 # endif // USE_IPHONE
2242
2243
2244   //  <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2245   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2246   [node2 setAttributesAsDictionary:
2247            @{ @"id":     @"textLiteral",
2248               @"arg":    @"-text-literal %",
2249 # ifdef USE_IPHONE
2250               @"_label": @"Text to display"
2251 # endif
2252             }];
2253   [self makeTextField:node2 on:rgroup 
2254 # ifndef USE_IPHONE
2255         withLabel:NO
2256 # else
2257         withLabel:YES
2258 # endif
2259         horizontal:NO];
2260   [node2 release];
2261
2262 //  rect = [last_child(rgroup) frame];
2263
2264 /* // trying to make the text fields be enabled only when the checkbox is on..
2265   control = last_child (rgroup);
2266   [control bind:@"enabled"
2267            toObject:[matrix cellAtRow:1 column:0]
2268            withKeyPath:@"value"
2269            options:nil];
2270  */
2271
2272
2273 # ifndef USE_IPHONE
2274   //  <file id="textFile" _label="" arg-set="-text-file %"/>
2275   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2276   [node2 setAttributesAsDictionary:
2277            @{ @"id":  @"textFile",
2278               @"arg": @"-text-file %" }];
2279   [self makeFileSelector:node2 on:rgroup
2280         dirsOnly:NO withLabel:NO editable:NO];
2281   [node2 release];
2282 # endif // !USE_IPHONE
2283
2284 //  rect = [last_child(rgroup) frame];
2285
2286   //  <string id="textURL" _label="" arg-set="text-url %"/>
2287   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2288   [node2 setAttributesAsDictionary:
2289            @{ @"id":     @"textURL",            
2290               @"arg":    @"-text-url %",
2291 # ifdef USE_IPHONE
2292               @"_label": @"URL to display",     
2293 # endif
2294             }];
2295   [self makeTextField:node2 on:rgroup 
2296 # ifndef USE_IPHONE
2297         withLabel:NO
2298 # else
2299         withLabel:YES
2300 # endif
2301         horizontal:NO];
2302   [node2 release];
2303
2304 //  rect = [last_child(rgroup) frame];
2305
2306 # ifndef USE_IPHONE
2307   if (program_p) {
2308     //  <string id="textProgram" _label="" arg-set="text-program %"/>
2309     node2 = [[NSXMLElement alloc] initWithName:@"string"];
2310     [node2 setAttributesAsDictionary:
2311              @{ @"id":   @"textProgram",
2312                  @"arg": @"-text-program %",
2313               }];
2314     [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2315     [node2 release];
2316   }
2317
2318 //  rect = [last_child(rgroup) frame];
2319
2320   layout_group (rgroup, NO);
2321
2322   rect = [rgroup frame];
2323   rect.size.width += 35;    // WTF?  Why is rgroup too narrow?
2324   [rgroup setFrame:rect];
2325
2326
2327   // Set the height of the cells in the radio-box matrix to the height of
2328   // the (last of the) text fields.
2329   control = last_child (rgroup);
2330   rect = [control frame];
2331   rect.size.width = 30;  // width of the string "Text", plus a bit...
2332   if (program_p)
2333     rect.size.width += 25;
2334   rect.size.height += LINE_SPACING;
2335   [matrix setCellSize:rect.size];
2336   [matrix sizeToCells];
2337
2338   layout_group (group, YES);
2339   rect = [matrix frame];
2340   rect.origin.x += rect.size.width + COLUMN_SPACING;
2341   rect.origin.y -= [control frame].size.height - LINE_SPACING;
2342   [rgroup setFrameOrigin:rect.origin];
2343
2344   // now cheat on the size of the matrix: allow it to overlap (underlap)
2345   // the text fields.
2346   // 
2347   rect.size = [matrix cellSize];
2348   rect.size.width = 300;
2349   [matrix setCellSize:rect.size];
2350   [matrix sizeToCells];
2351
2352   // Cheat on the position of the stuff on the right (the rgroup).
2353   // GAAAH, this code is such crap!
2354   rect = [rgroup frame];
2355   rect.origin.y -= 5;
2356   [rgroup setFrame:rect];
2357
2358
2359   rect.size.width = rect.size.height = 0;
2360   NSBox *box = [[NSBox alloc] initWithFrame:rect];
2361   [box setTitlePosition:NSAtTop];
2362   [box setBorderType:NSBezelBorder];
2363   [box setTitle:@"Display Text"];
2364
2365   rect.size.width = rect.size.height = 12;
2366   [box setContentViewMargins:rect.size];
2367   [box setContentView:group];
2368   [box sizeToFit];
2369
2370   [self placeChild:box on:parent];
2371   [group release];
2372   [box release];
2373
2374 # endif // !USE_IPHONE
2375 }
2376
2377
2378 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2379 {
2380   /*
2381     [x]  Grab desktop images
2382     [ ]  Choose random image:
2383          [__________________________]  [Choose]
2384
2385    <boolean id="grabDesktopImages" _label="Grab desktop images"
2386        arg-unset="-no-grab-desktop"/>
2387    <boolean id="chooseRandomImages" _label="Grab desktop images"
2388        arg-unset="-choose-random-images"/>
2389    <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2390    */
2391
2392   NSXMLElement *node2;
2393
2394 # ifndef USE_IPHONE
2395 #  define SCREENS "Grab desktop images"
2396 #  define PHOTOS  "Choose random images"
2397 # else
2398 #  define SCREENS "Grab screenshots"
2399 #  define PHOTOS  "Use photo library"
2400 # endif
2401
2402   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2403   [node2 setAttributesAsDictionary:
2404            @{ @"id":        @"grabDesktopImages",
2405               @"_label":    @ SCREENS,
2406               @"arg-unset": @"-no-grab-desktop",
2407             }];
2408   [self makeCheckbox:node2 on:parent];
2409   [node2 release];
2410
2411   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2412   [node2 setAttributesAsDictionary:
2413            @{ @"id":      @"chooseRandomImages",
2414               @"_label":  @ PHOTOS,
2415               @"arg-set": @"-choose-random-images",
2416             }];
2417   [self makeCheckbox:node2 on:parent];
2418   [node2 release];
2419
2420   node2 = [[NSXMLElement alloc] initWithName:@"string"];
2421   [node2 setAttributesAsDictionary:
2422            @{ @"id":     @"imageDirectory",
2423               @"_label": @"Images from:",
2424               @"arg":    @"-image-directory %",
2425             }];
2426   [self makeFileSelector:node2 on:parent
2427         dirsOnly:YES withLabel:YES editable:YES];
2428   [node2 release];
2429
2430 # undef SCREENS
2431 # undef PHOTOS
2432
2433 # ifndef USE_IPHONE
2434   // Add a second, explanatory label below the file/URL selector.
2435
2436   LABEL *lab2 = 0;
2437   lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2438   [self placeChild:lab2 on:parent];
2439
2440   // Pack it in a little tighter vertically.
2441   NSRect r2 = [lab2 frame];
2442   r2.origin.x += 20;
2443   r2.origin.y += 14;
2444   [lab2 setFrameOrigin:r2.origin];
2445 # endif // USE_IPHONE
2446 }
2447
2448
2449 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2450 {
2451 # ifndef USE_IPHONE
2452   /*
2453     [x]  Check for Updates  [ Monthly ]
2454
2455   <hgroup>
2456    <boolean id="automaticallyChecksForUpdates"
2457             _label="Automatically check for updates"
2458             arg-unset="-no-automaticallyChecksForUpdates" />
2459    <select id="updateCheckInterval">
2460     <option="hourly"  _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2461     <option="daily"   _label="Daily"  arg-set="-updateCheckInterval 86400"/>
2462     <option="weekly"  _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2463     <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2464    </select>
2465   </hgroup>
2466    */
2467
2468   // <hgroup>
2469
2470   NSRect rect;
2471   rect.size.width = rect.size.height = 1;
2472   rect.origin.x = rect.origin.y = 0;
2473   NSView *group = [[NSView alloc] initWithFrame:rect];
2474
2475   NSXMLElement *node2;
2476
2477   // <boolean ...>
2478
2479   node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2480   [node2 setAttributesAsDictionary:
2481            @{ @"id":        @SUSUEnableAutomaticChecksKey,
2482               @"_label":    @"Automatically check for updates",
2483               @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2484             }];
2485   [self makeCheckbox:node2 on:group];
2486   [node2 release];
2487
2488   // <select ...>
2489
2490   node2 = [[NSXMLElement alloc] initWithName:@"select"];
2491   [node2 setAttributesAsDictionary:
2492            @{ @"id": @SUScheduledCheckIntervalKey }];
2493
2494   //   <option ...>
2495
2496   NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2497   [node3 setAttributesAsDictionary:
2498            @{ @"id":      @"hourly",
2499               @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2500               @"_label":  @"Hourly" }];
2501   [node3 setParent: node2];
2502   [node3 autorelease];
2503
2504   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2505   [node3 setAttributesAsDictionary:
2506            @{ @"id":      @"daily",
2507               @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2508               @"_label":  @"Daily" }];
2509   [node3 setParent: node2];
2510   [node3 autorelease];
2511
2512   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2513   [node3 setAttributesAsDictionary:
2514            @{ @"id": @"weekly",
2515            // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2516               @"_label": @"Weekly",
2517             }];
2518   [node3 setParent: node2];
2519   [node3 autorelease];
2520
2521   node3 = [[NSXMLElement alloc] initWithName:@"option"];
2522   [node3 setAttributesAsDictionary:
2523            @{ @"id":      @"monthly",
2524               @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2525               @"_label":  @"Monthly",
2526              }];
2527   [node3 setParent: node2];
2528   [node3 autorelease];
2529
2530   // </option>
2531   [self makeOptionMenu:node2 on:group];
2532   [node2 release];
2533
2534   // </hgroup>
2535   layout_group (group, TRUE);
2536
2537   rect.size.width = rect.size.height = 0;
2538   NSBox *box = [[NSBox alloc] initWithFrame:rect];
2539   [box setTitlePosition:NSNoTitle];
2540   [box setBorderType:NSNoBorder];
2541   [box setContentViewMargins:rect.size];
2542   [box setContentView:group];
2543   [box sizeToFit];
2544
2545   [self placeChild:box on:parent];
2546
2547   [group release];
2548   [box release];
2549
2550 # endif // !USE_IPHONE
2551 }
2552
2553
2554 #pragma mark Layout for controls
2555
2556
2557 # ifndef USE_IPHONE
2558 static NSView *
2559 last_child (NSView *parent)
2560 {
2561   NSArray *kids = [parent subviews];
2562   NSUInteger nkids = [kids count];
2563   if (nkids == 0)
2564     return 0;
2565   else
2566     return [kids objectAtIndex:nkids-1];
2567 }
2568 #endif // USE_IPHONE
2569
2570
2571 /* Add the child as a subview of the parent, positioning it immediately
2572    below or to the right of the previously-added child of that view.
2573  */
2574 - (void) placeChild:
2575 # ifdef USE_IPHONE
2576         (NSObject *)child
2577 # else
2578         (NSView *)child
2579 # endif
2580         on:(NSView *)parent right:(BOOL)right_p
2581 {
2582 # ifndef USE_IPHONE
2583   NSRect rect = [child frame];
2584   NSView *last = last_child (parent);
2585   if (!last) {
2586     rect.origin.x = LEFT_MARGIN;
2587     rect.origin.y = ([parent frame].size.height - rect.size.height 
2588                      - LINE_SPACING);
2589   } else if (right_p) {
2590     rect = [last frame];
2591     rect.origin.x += rect.size.width + COLUMN_SPACING;
2592   } else {
2593     rect = [last frame];
2594     rect.origin.x = LEFT_MARGIN;
2595     rect.origin.y -= [child frame].size.height + LINE_SPACING;
2596   }
2597   NSRect r = [child frame];
2598   r.origin = rect.origin;
2599   [child setFrame:r];
2600   [parent addSubview:child];
2601
2602 # else // USE_IPHONE
2603
2604   /* Controls is an array of arrays of the controls, divided into sections.
2605      Each hgroup / vgroup gets a nested array, too, e.g.:
2606
2607        [ [ [ <label>, <checkbox> ],
2608            [ <label>, <checkbox> ],
2609            [ <label>, <checkbox> ] ],
2610          [ <label>, <text-field> ],
2611          [ <label>, <low-label>, <slider>, <high-label> ],
2612          [ <low-label>, <slider>, <high-label> ],
2613          <HTML-label>
2614        ];
2615
2616      If an element begins with a label, it is terminal, otherwise it is a
2617      group.  There are (currently) never more than 4 elements in a single
2618      terminal element.
2619
2620      A blank vertical spacer is placed between each hgroup / vgroup,
2621      by making each of those a new section in the TableView.
2622    */
2623   if (! controls)
2624     controls = [[NSMutableArray arrayWithCapacity:10] retain];
2625   if ([controls count] == 0)
2626     [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2627   NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2628
2629   if (!right_p || [current count] == 0) {
2630     // Nothing on the current line. Add this object.
2631     [current addObject: child];
2632   } else {
2633     // Something's on the current line already.
2634     NSObject *old = [current objectAtIndex:[current count]-1];
2635     if ([old isKindOfClass:[NSMutableArray class]]) {
2636       // Already an array in this cell. Append.
2637       NSAssert ([(NSArray *) old count] < 4, @"internal error");
2638       [(NSMutableArray *) old addObject: child];
2639     } else {
2640       // Replace the control in this cell with an array, then append
2641       NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2642       [current replaceObjectAtIndex:[current count]-1 withObject:a];
2643     }
2644   }
2645 # endif // USE_IPHONE
2646 }
2647
2648
2649 - (void) placeChild:(NSView *)child on:(NSView *)parent
2650 {
2651   [self placeChild:child on:parent right:NO];
2652 }
2653
2654
2655 #ifdef USE_IPHONE
2656
2657 // Start putting subsequent children in a new group, to create a new
2658 // section on the UITableView.
2659 //
2660 - (void) placeSeparator
2661 {
2662   if (! controls) return;
2663   if ([controls count] == 0) return;
2664   if ([[controls objectAtIndex:[controls count]-1]
2665         count] > 0)
2666     [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2667 }
2668 #endif // USE_IPHONE
2669
2670
2671
2672 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2673    wrapped in <hgroup> or <vgroup> in the XML.
2674  */
2675 - (void) makeGroup:(NSXMLNode *)node 
2676                 on:(NSView *)parent
2677         horizontal:(BOOL) horiz_p
2678 {
2679 # ifdef USE_IPHONE
2680   if (!horiz_p) [self placeSeparator];
2681   [self traverseChildren:node on:parent];
2682   if (!horiz_p) [self placeSeparator];
2683 # else  // !USE_IPHONE
2684   NSRect rect;
2685   rect.size.width = rect.size.height = 1;
2686   rect.origin.x = rect.origin.y = 0;
2687   NSView *group = [[NSView alloc] initWithFrame:rect];
2688   [self traverseChildren:node on:group];
2689
2690   layout_group (group, horiz_p);
2691
2692   rect.size.width = rect.size.height = 0;
2693   NSBox *box = [[NSBox alloc] initWithFrame:rect];
2694   [box setTitlePosition:NSNoTitle];
2695   [box setBorderType:NSNoBorder];
2696   [box setContentViewMargins:rect.size];
2697   [box setContentView:group];
2698   [box sizeToFit];
2699
2700   [self placeChild:box on:parent];
2701   [group release];
2702   [box release];
2703 # endif // !USE_IPHONE
2704 }
2705
2706
2707 #ifndef USE_IPHONE
2708 static void
2709 layout_group (NSView *group, BOOL horiz_p)
2710 {
2711   NSArray *kids = [group subviews];
2712   NSUInteger nkids = [kids count];
2713   NSUInteger i;
2714   double maxx = 0, miny = 0;
2715   for (i = 0; i < nkids; i++) {
2716     NSView *kid = [kids objectAtIndex:i];
2717     NSRect r = [kid frame];
2718     
2719     if (horiz_p) {
2720       maxx += r.size.width + COLUMN_SPACING;
2721       if (r.size.height > -miny) miny = -r.size.height;
2722     } else {
2723       if (r.size.width > maxx)  maxx = r.size.width;
2724       miny = r.origin.y - r.size.height;
2725     }
2726   }
2727   
2728   NSRect rect;
2729   rect.origin.x = 0;
2730   rect.origin.y = 0;
2731   rect.size.width = maxx;
2732   rect.size.height = -miny;
2733   [group setFrame:rect];
2734
2735   double x = 0;
2736   for (i = 0; i < nkids; i++) {
2737     NSView *kid = [kids objectAtIndex:i];
2738     NSRect r = [kid frame];
2739     if (horiz_p) {
2740       r.origin.y = rect.size.height - r.size.height;
2741       r.origin.x = x;
2742       x += r.size.width + COLUMN_SPACING;
2743     } else {
2744       r.origin.y -= miny;
2745     }
2746     [kid setFrame:r];
2747   }
2748 }
2749 #endif // !USE_IPHONE
2750
2751
2752 /* Create some kind of control corresponding to the given XML node.
2753  */
2754 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2755 {
2756   NSString *name = [node name];
2757
2758   if ([node kind] == NSXMLCommentKind)
2759     return;
2760
2761   if ([node kind] == NSXMLTextKind) {
2762     NSString *s = [(NSString *) [node objectValue]
2763                    stringByTrimmingCharactersInSet:
2764                     [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2765     if (! [s isEqualToString:@""]) {
2766       NSAssert1 (0, @"unexpected text: %@", s);
2767     }
2768     return;
2769   }
2770
2771   if ([node kind] != NSXMLElementKind) {
2772     NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2773     return;
2774   }
2775
2776   if ([name isEqualToString:@"hgroup"] ||
2777       [name isEqualToString:@"vgroup"]) {
2778
2779     [self makeGroup:node on:parent 
2780           horizontal:[name isEqualToString:@"hgroup"]];
2781
2782   } else if ([name isEqualToString:@"command"]) {
2783     // do nothing: this is the "-root" business
2784
2785   } else if ([name isEqualToString:@"video"]) {
2786     // ignored
2787
2788   } else if ([name isEqualToString:@"boolean"]) {
2789     [self makeCheckbox:node on:parent];
2790
2791   } else if ([name isEqualToString:@"string"]) {
2792     [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2793
2794   } else if ([name isEqualToString:@"file"]) {
2795     [self makeFileSelector:node on:parent
2796           dirsOnly:NO withLabel:YES editable:NO];
2797
2798   } else if ([name isEqualToString:@"number"]) {
2799     [self makeNumberSelector:node on:parent];
2800
2801   } else if ([name isEqualToString:@"select"]) {
2802     [self makeOptionMenu:node on:parent];
2803
2804   } else if ([name isEqualToString:@"_description"]) {
2805     [self makeDescLabel:node on:parent];
2806
2807   } else if ([name isEqualToString:@"xscreensaver-text"]) {
2808     [self makeTextLoaderControlBox:node on:parent];
2809
2810   } else if ([name isEqualToString:@"xscreensaver-image"]) {
2811     [self makeImageLoaderControlBox:node on:parent];
2812
2813   } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2814     [self makeUpdaterControlBox:node on:parent];
2815
2816   } else {
2817     NSAssert1 (0, @"unknown tag: %@", name);
2818   }
2819 }
2820
2821
2822 /* Iterate over and process the children of this XML node.
2823  */
2824 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2825 {
2826   NSArray *children = [node children];
2827   NSUInteger i, count = [children count];
2828   for (i = 0; i < count; i++) {
2829     NSXMLNode *child = [children objectAtIndex:i];
2830     [self makeControl:child on:parent];
2831   }
2832 }
2833
2834
2835 # ifndef USE_IPHONE
2836
2837 /* Kludgey magic to make the window enclose the controls we created.
2838  */
2839 static void
2840 fix_contentview_size (NSView *parent)
2841 {
2842   NSRect f;
2843   NSArray *kids = [parent subviews];
2844   NSUInteger nkids = [kids count];
2845   NSView *text = 0;  // the NSText at the bottom of the window
2846   double maxx = 0, miny = 0;
2847   NSUInteger i;
2848
2849   /* Find the size of the rectangle taken up by each of the children
2850      except the final "NSText" child.
2851   */
2852   for (i = 0; i < nkids; i++) {
2853     NSView *kid = [kids objectAtIndex:i];
2854     if ([kid isKindOfClass:[NSText class]]) {
2855       text = kid;
2856       continue;
2857     }
2858     f = [kid frame];
2859     if (f.origin.x + f.size.width > maxx)  maxx = f.origin.x + f.size.width;
2860     if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2861 //    NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2862 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
2863 //          f.origin.y + f.size.height, [kid class]);
2864   }
2865   
2866   if (maxx < 400) maxx = 400;   // leave room for the NSText paragraph...
2867   
2868   /* Now that we know the width of the window, set the width of the NSText to
2869      that, so that it can decide what its height needs to be.
2870    */
2871   if (! text) abort();
2872   f = [text frame];
2873 //  NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2874 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
2875 //        f.origin.y + f.size.height, [text class]);
2876   
2877   // set the NSText's width (this changes its height).
2878   f.size.width = maxx - LEFT_MARGIN;
2879   [text setFrame:f];
2880   
2881   // position the NSText below the last child (this gives us a new miny).
2882   f = [text frame];
2883   f.origin.y = miny - f.size.height - LINE_SPACING;
2884   miny = f.origin.y - LINE_SPACING;
2885   [text setFrame:f];
2886   
2887   // Lock the width of the field and unlock the height, and let it resize
2888   // once more, to compute the proper height of the text for that width.
2889   //
2890   [(NSText *) text setHorizontallyResizable:NO];
2891   [(NSText *) text setVerticallyResizable:YES];
2892   [(NSText *) text sizeToFit];
2893
2894   // Now lock the height too: no more resizing this text field.
2895   //
2896   [(NSText *) text setVerticallyResizable:NO];
2897
2898   // Now reposition the top edge of the text field to be back where it
2899   // was before we changed the height.
2900   //
2901   float oh = f.size.height;
2902   f = [text frame];
2903   float dh = f.size.height - oh;
2904   f.origin.y += dh;
2905
2906   // #### This is needed in OSX 10.5, but is wrong in OSX 10.6.  WTF??
2907   //      If we do this in 10.6, the text field moves down, off the window.
2908   //      So instead we repair it at the end, at the "WTF2" comment.
2909   [text setFrame:f];
2910
2911   // Also adjust the parent height by the change in height of the text field.
2912   miny -= dh;
2913
2914 //  NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2915 //        f.size.width, f.size.height, f.origin.x, f.origin.y,
2916 //        f.origin.y + f.size.height, [text class]);
2917   
2918   
2919   /* Set the contentView to the size of the children.
2920    */
2921   f = [parent frame];
2922 //  float yoff = f.size.height;
2923   f.size.width = maxx + LEFT_MARGIN;
2924   f.size.height = -(miny - LEFT_MARGIN*2);
2925 //  yoff = f.size.height - yoff;
2926   [parent setFrame:f];
2927
2928 //  NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", 
2929 //        f.size.width, f.size.height, f.origin.x, f.origin.y);
2930
2931   /* Now move all of the kids up into the window.
2932    */
2933   f = [parent frame];
2934   float shift = f.size.height;
2935 //  NSLog(@"shift: %3.0f", shift);
2936   for (i = 0; i < nkids; i++) {
2937     NSView *kid = [kids objectAtIndex:i];
2938     f = [kid frame];
2939     f.origin.y += shift;
2940     [kid setFrame:f];
2941 //    NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f  %3.0f  %@",
2942 //          f.size.width, f.size.height, f.origin.x, f.origin.y,
2943 //          f.origin.y + f.size.height, [kid class]);
2944   }
2945   
2946 /*
2947     Bad:
2948      parent: 420 x 541 @   0   0
2949      text:   380 x 100 @  20  22  miny=-501
2950
2951     Good:
2952      parent: 420 x 541 @   0   0
2953      text:   380 x 100 @  20  50  miny=-501
2954  */
2955
2956   // #### WTF2: See "WTF" above.  If the text field is off the screen,
2957   //      move it up.  We need this on 10.6 but not on 10.5.  Auugh.
2958   //
2959   f = [text frame];
2960   if (f.origin.y < 50) {    // magic numbers, yay
2961     f.origin.y = 50;
2962     [text setFrame:f];
2963   }
2964
2965   /* Set the kids to track the top left corner of the window when resized.
2966      Set the NSText to track the bottom right corner as well.
2967    */
2968   for (i = 0; i < nkids; i++) {
2969     NSView *kid = [kids objectAtIndex:i];
2970     unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2971     if ([kid isKindOfClass:[NSText class]])
2972       mask |= NSViewWidthSizable|NSViewHeightSizable;
2973     [kid setAutoresizingMask:mask];
2974   }
2975 }
2976 # endif // !USE_IPHONE
2977
2978
2979
2980 #ifndef USE_IPHONE
2981 static NSView *
2982 wrap_with_buttons (NSWindow *window, NSView *panel)
2983 {
2984   NSRect rect;
2985   
2986   // Make a box to hold the buttons at the bottom of the window.
2987   //
2988   rect = [panel frame];
2989   rect.origin.x = rect.origin.y = 0;
2990   rect.size.height = 10;
2991   NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2992   [bbox setTitlePosition:NSNoTitle];  
2993   [bbox setBorderType:NSNoBorder];
2994   
2995   // Make some buttons: Default, Cancel, OK
2996   //
2997   rect.origin.x = rect.origin.y = 0;
2998   rect.size.width = rect.size.height = 10;
2999   NSButton *reset = [[NSButton alloc] initWithFrame:rect];
3000   [reset setTitle:@"Reset to Defaults"];
3001   [reset setBezelStyle:NSRoundedBezelStyle];
3002   [reset sizeToFit];
3003
3004   rect = [reset frame];
3005   NSButton *ok = [[NSButton alloc] initWithFrame:rect];
3006   [ok setTitle:@"OK"];
3007   [ok setBezelStyle:NSRoundedBezelStyle];
3008   [ok sizeToFit];
3009   rect = [bbox frame];
3010   rect.origin.x = rect.size.width - [ok frame].size.width;
3011   [ok setFrameOrigin:rect.origin];
3012
3013   rect = [ok frame];
3014   NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
3015   [cancel setTitle:@"Cancel"];
3016   [cancel setBezelStyle:NSRoundedBezelStyle];
3017   [cancel sizeToFit];
3018   rect.origin.x -= [cancel frame].size.width + 10;
3019   [cancel setFrameOrigin:rect.origin];
3020
3021   // Bind OK to RET and Cancel to ESC.
3022   [ok     setKeyEquivalent:@"\r"];
3023   [cancel setKeyEquivalent:@"\e"];
3024
3025   // The correct width for OK and Cancel buttons is 68 pixels
3026   // ("Human Interface Guidelines: Controls: Buttons: 
3027   // Push Button Specifications").
3028   //
3029   rect = [ok frame];
3030   rect.size.width = 68;
3031   [ok setFrame:rect];
3032
3033   rect = [cancel frame];
3034   rect.size.width = 68;
3035   [cancel setFrame:rect];
3036
3037   // It puts the buttons in the box or else it gets the hose again
3038   //
3039   [bbox addSubview:ok];
3040   [bbox addSubview:cancel];
3041   [bbox addSubview:reset];
3042   [bbox sizeToFit];
3043   
3044   // make a box to hold the button-box, and the preferences view
3045   //
3046   rect = [bbox frame];
3047   rect.origin.y += rect.size.height;
3048   NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
3049   [pbox setTitlePosition:NSNoTitle];
3050   [pbox setBorderType:NSBezelBorder];
3051
3052   // Enforce a max height on the dialog, so that it's obvious to me
3053   // (on a big screen) when the dialog will fall off the bottom of
3054   // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
3055   {
3056     NSRect f = [panel frame];
3057     int screen_height = (768    // shortest "modern" Mac display
3058                          - 22   // menu bar
3059                          - 56   // System Preferences toolbar
3060                          - 140  // default magnified bottom dock icon
3061                          );
3062     if (f.size.height > screen_height) {
3063       NSLog(@"%@ height was %.0f; clipping to %d", 
3064           [panel class], f.size.height, screen_height);
3065       f.size.height = screen_height;
3066       [panel setFrame:f];
3067     }
3068   }
3069
3070   [pbox addSubview:panel];
3071   [pbox addSubview:bbox];
3072   [pbox sizeToFit];
3073
3074   [reset  setAutoresizingMask:NSViewMaxXMargin];
3075   [cancel setAutoresizingMask:NSViewMinXMargin];
3076   [ok     setAutoresizingMask:NSViewMinXMargin];
3077   [bbox   setAutoresizingMask:NSViewWidthSizable];
3078   
3079   // grab the clicks
3080   //
3081   [ok     setTarget:window];
3082   [cancel setTarget:window];
3083   [reset  setTarget:window];
3084   [ok     setAction:@selector(okAction:)];
3085   [cancel setAction:@selector(cancelAction:)];
3086   [reset  setAction:@selector(resetAction:)];
3087   
3088   [bbox release];
3089
3090   return pbox;
3091 }
3092 #endif // !USE_IPHONE
3093
3094
3095 /* Iterate over and process the children of the root node of the XML document.
3096  */
3097 - (void)traverseTree
3098 {
3099 # ifdef USE_IPHONE
3100   NSView *parent = [self view];
3101 # else
3102   NSWindow *parent = self;
3103 #endif
3104   NSXMLNode *node = xml_root;
3105
3106   if (![[node name] isEqualToString:@"screensaver"]) {
3107     NSAssert (0, @"top level node is not <xscreensaver>");
3108   }
3109
3110   saver_name = [self parseXScreenSaverTag: node];
3111   saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
3112                            withString:@""];
3113   [saver_name retain];
3114   
3115 # ifndef USE_IPHONE
3116
3117   NSRect rect;
3118   rect.origin.x = rect.origin.y = 0;
3119   rect.size.width = rect.size.height = 1;
3120
3121   NSView *panel = [[NSView alloc] initWithFrame:rect];
3122   [self traverseChildren:node on:panel];
3123   fix_contentview_size (panel);
3124
3125   NSView *root = wrap_with_buttons (parent, panel);
3126   [userDefaultsController   setAppliesImmediately:NO];
3127   [globalDefaultsController setAppliesImmediately:NO];
3128
3129   [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
3130
3131   rect = [parent frameRectForContentRect:[root frame]];
3132   [parent setFrame:rect display:NO];
3133   [parent setMinSize:rect.size];
3134   
3135   [parent setContentView:root];
3136         
3137   [panel release];
3138   [root release];
3139
3140 # else  // USE_IPHONE
3141
3142   CGRect r = [parent frame];
3143   r.size = [[UIScreen mainScreen] bounds].size;
3144   [parent setFrame:r];
3145   [self traverseChildren:node on:parent];
3146
3147 # endif // USE_IPHONE
3148 }
3149
3150
3151 - (void)parser:(NSXMLParser *)parser
3152         didStartElement:(NSString *)elt
3153         namespaceURI:(NSString *)ns
3154         qualifiedName:(NSString *)qn
3155         attributes:(NSDictionary *)attrs
3156 {
3157   NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3158   [e autorelease];
3159   [e setKind:SimpleXMLElementKind];
3160   [e setAttributesAsDictionary:attrs];
3161   NSXMLElement *p = xml_parsing;
3162   [e setParent:p];
3163   xml_parsing = e;
3164   if (! xml_root)
3165     xml_root = xml_parsing;
3166 }
3167
3168 - (void)parser:(NSXMLParser *)parser
3169         didEndElement:(NSString *)elt
3170         namespaceURI:(NSString *)ns
3171         qualifiedName:(NSString *)qn
3172 {
3173   NSXMLElement *p = xml_parsing;
3174   if (! p) {
3175     NSLog(@"extra close: %@", elt);
3176   } else if (![[p name] isEqualToString:elt]) {
3177     NSLog(@"%@ closed by %@", [p name], elt);
3178   } else {
3179     NSXMLElement *n = xml_parsing;
3180     xml_parsing = [n parent];
3181   }
3182 }
3183
3184
3185 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3186 {
3187   NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3188   [e setKind:SimpleXMLTextKind];
3189   NSXMLElement *p = xml_parsing;
3190   [e setParent:p];
3191   [e setObjectValue: string];
3192   [e autorelease];
3193 }
3194
3195
3196 # ifdef USE_IPHONE
3197 # ifdef USE_PICKER_VIEW
3198
3199 #pragma mark UIPickerView delegate methods
3200
3201 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3202 {
3203   return 1;     // Columns
3204 }
3205
3206 - (NSInteger)pickerView:(UIPickerView *)pv
3207              numberOfRowsInComponent:(NSInteger)column
3208 {
3209   NSAssert (column == 0, @"weird column");
3210   NSArray *a = [picker_values objectAtIndex: [pv tag]];
3211   if (! a) return 0;  // Too early?
3212   return [a count];
3213 }
3214
3215 - (CGFloat)pickerView:(UIPickerView *)pv
3216            rowHeightForComponent:(NSInteger)column
3217 {
3218   return FONT_SIZE;
3219 }
3220
3221 - (CGFloat)pickerView:(UIPickerView *)pv
3222            widthForComponent:(NSInteger)column
3223 {
3224   NSAssert (column == 0, @"weird column");
3225   NSArray *a = [picker_values objectAtIndex: [pv tag]];
3226   if (! a) return 0;  // Too early?
3227
3228   UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3229   CGFloat max = 0;
3230   for (NSArray *a2 in a) {
3231     NSString *s = [a2 objectAtIndex:0];
3232     // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize.
3233     CGSize r = [s sizeWithFont:f];
3234     if (r.width > max) max = r.width;
3235   }
3236
3237   max *= 1.7;   // WTF!!
3238
3239   if (max > 320)
3240     max = 320;
3241   else if (max < 120)
3242     max = 120;
3243
3244   return max;
3245
3246 }
3247
3248
3249 - (NSString *)pickerView:(UIPickerView *)pv
3250               titleForRow:(NSInteger)row
3251               forComponent:(NSInteger)column
3252 {
3253   NSAssert (column == 0, @"weird column");
3254   NSArray *a = [picker_values objectAtIndex: [pv tag]];
3255   if (! a) return 0;  // Too early?
3256   a = [a objectAtIndex:row];
3257   NSAssert (a, @"internal error");
3258   return [a objectAtIndex:0];
3259 }
3260
3261 # endif // USE_PICKER_VIEW
3262
3263
3264 #pragma mark UITableView delegate methods
3265
3266 - (void) addResetButton
3267 {
3268   [[self navigationItem] 
3269     setRightBarButtonItem: [[UIBarButtonItem alloc]
3270                              initWithTitle: @"Reset to Defaults"
3271                              style: UIBarButtonItemStylePlain
3272                              target:self
3273                              action:@selector(resetAction:)]];
3274   NSString *s = saver_name;
3275   if ([self view].frame.size.width > 320)
3276     s = [s stringByAppendingString: @" Settings"];
3277   [self navigationItem].title = s;
3278 }
3279
3280
3281 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3282 {
3283   return YES;
3284 }
3285
3286 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3287   // Number of vertically-stacked white boxes.
3288   return [controls count];
3289 }
3290
3291 - (NSInteger)tableView:(UITableView *)tableView
3292              numberOfRowsInSection:(NSInteger)section
3293 {
3294   // Number of lines in each vertically-stacked white box.
3295   NSAssert (controls, @"internal error");
3296   return [[controls objectAtIndex:section] count];
3297 }
3298
3299 - (NSString *)tableView:(UITableView *)tv
3300               titleForHeaderInSection:(NSInteger)section
3301 {
3302   // Titles above each vertically-stacked white box.
3303 //  if (section == 0)
3304 //    return [saver_name stringByAppendingString:@" Settings"];
3305   return nil;
3306 }
3307
3308
3309 - (CGFloat)tableView:(UITableView *)tv
3310            heightForRowAtIndexPath:(NSIndexPath *)ip
3311 {
3312   CGFloat h = 0;
3313
3314   NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3315                   objectAtIndex:[ip indexAtPosition:1]];
3316
3317   if ([ctl isKindOfClass:[NSArray class]]) {
3318     NSArray *set = (NSArray *) ctl;
3319     switch ([set count]) {
3320     case 4:                     // label + left/slider/right.
3321     case 3:                     // left/slider/right.
3322       h = FONT_SIZE * 3.0;
3323       break;
3324     case 2:                     // Checkboxes, or text fields.
3325       h = FONT_SIZE * 2.4;
3326       break;
3327     }
3328   } else if ([ctl isKindOfClass:[UILabel class]]) {
3329     // Radio buttons in a multi-select list.
3330     h = FONT_SIZE * 1.9;
3331
3332 # ifdef USE_HTML_LABELS
3333   } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3334     
3335     HTMLLabel *t = (HTMLLabel *) ctl;
3336     CGRect r = t.frame;
3337     r.size.width = [tv frame].size.width;
3338     r.size.width -= LEFT_MARGIN * 2;
3339     [t setFrame:r];
3340     [t sizeToFit];
3341     r = t.frame;
3342     h = r.size.height;
3343 # endif // USE_HTML_LABELS
3344
3345   } else {                      // Does this ever happen?
3346     h = FONT_SIZE + LINE_SPACING * 2;
3347   }
3348
3349   if (h <= 0) abort();
3350   return h;
3351 }
3352
3353
3354 - (void)refreshTableView
3355 {
3356   UITableView *tv = (UITableView *) [self view];
3357   NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3358   NSInteger rows = [self numberOfSectionsInTableView:tv];
3359   for (int i = 0; i < rows; i++) {
3360     NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3361     for (int j = 0; j < cols; j++) {
3362       NSUInteger ip[2];
3363       ip[0] = i;
3364       ip[1] = j;
3365       [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3366     }
3367   }
3368
3369   [tv beginUpdates];
3370   [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3371   [tv endUpdates];
3372
3373   // Default opacity looks bad.
3374   // #### Oh great, this only works *sometimes*.
3375   UIView *v = [[self navigationItem] titleView];
3376   [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
3377 }
3378
3379
3380 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3381 {
3382   [NSTimer scheduledTimerWithTimeInterval: 0
3383            target:self
3384            selector:@selector(refreshTableView)
3385            userInfo:nil
3386            repeats:NO];
3387 }
3388
3389
3390 #ifndef USE_PICKER_VIEW
3391
3392 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3393                       button:(RadioButton *)b
3394 {
3395   NSArray *item = [[b items] objectAtIndex: [b index]];
3396   NSString *pref_key = [item objectAtIndex:1];
3397   NSObject *pref_val = [item objectAtIndex:2];
3398
3399   NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3400
3401   // Convert them both to strings and compare those, so that
3402   // we don't get screwed by int 1 versus string "1".
3403   // Will boolean true/1 screw us here too?
3404   //
3405   NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3406                         ? (NSString *) pref_val
3407                         : [(NSNumber *) pref_val stringValue]);
3408   NSString *current_str = ([current isKindOfClass:[NSString class]]
3409                            ? (NSString *) current
3410                            : [(NSNumber *) current stringValue]);
3411   BOOL match_p = [current_str isEqualToString:pref_str];
3412
3413   // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3414
3415   if (match_p)
3416     [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3417   else
3418     [cell setAccessoryType:UITableViewCellAccessoryNone];
3419 }
3420
3421
3422 - (void)tableView:(UITableView *)tv
3423         didSelectRowAtIndexPath:(NSIndexPath *)ip
3424 {
3425   RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3426                        objectAtIndex:[ip indexAtPosition:1]];
3427   if (! [ctl isKindOfClass:[RadioButton class]])
3428     return;
3429
3430   [self radioAction:ctl];
3431   [self refreshTableView];
3432 }
3433
3434
3435 #endif // !USE_PICKER_VIEW
3436
3437
3438
3439 - (UITableViewCell *)tableView:(UITableView *)tv
3440                      cellForRowAtIndexPath:(NSIndexPath *)ip
3441 {
3442   CGFloat ww = [tv frame].size.width;
3443   CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
3444
3445   float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
3446
3447   // Width of the column of labels on the left.
3448   CGFloat left_width = ww * 0.4;
3449   CGFloat right_edge = ww - LEFT_MARGIN;
3450
3451   if (os_version < 7)  // margins were wider on iOS 6.1
3452     right_edge -= 10;
3453
3454   CGFloat max = FONT_SIZE * 12;
3455   if (left_width > max) left_width = max;
3456
3457   NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3458                            objectAtIndex:[ip indexAtPosition:1]];
3459
3460   if ([ctl isKindOfClass:[NSArray class]]) {
3461     // This cell has a set of objects in it.
3462     NSArray *set = (NSArray *) ctl;
3463     switch ([set count]) {
3464     case 2:
3465       {
3466         // With 2 elements, the first of the pair must be a label.
3467         UILabel *label = (UILabel *) [set objectAtIndex: 0];
3468         NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3469         ctl = [set objectAtIndex: 1];
3470
3471         CGRect r = [ctl frame];
3472
3473         if ([ctl isKindOfClass:[UISwitch class]]) {     // Checkboxes.
3474           r.size.width = 80;  // Magic.
3475           r.origin.x = right_edge - r.size.width + 30;  // beats me
3476
3477           if (os_version < 7)  // checkboxes were wider on iOS 6.1
3478             r.origin.x -= 25;
3479
3480         } else {
3481           r.origin.x = left_width;                      // Text fields, etc.
3482           r.size.width = right_edge - r.origin.x;
3483         }
3484
3485         r.origin.y = (hh - r.size.height) / 2;   // Center vertically.
3486         [ctl setFrame:r];
3487
3488         // Make a box and put the label and checkbox/slider into it.
3489         r.origin.x = 0;
3490         r.origin.y = 0;
3491         r.size.width  = ww;
3492         r.size.height = hh;
3493         NSView *box = [[UIView alloc] initWithFrame:r];
3494         [box addSubview: ctl];
3495
3496         // Let the label make use of any space not taken up by the control.
3497         r = [label frame];
3498         r.origin.x = LEFT_MARGIN;
3499         r.origin.y = 0;
3500         r.size.width  = [ctl frame].origin.x - r.origin.x;
3501         r.size.height = hh;
3502         [label setFrame:r];
3503         [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3504         [box addSubview: label];
3505         [box autorelease];
3506
3507         ctl = box;
3508       }
3509       break;
3510     case 3:
3511     case 4:
3512       {
3513         // With 3 elements, 1 and 3 are labels.
3514         // With 4 elements, 1, 2 and 4 are labels.
3515         int i = 0;
3516         UILabel *top  = ([set count] == 4
3517                          ? [set objectAtIndex: i++]
3518                          : 0);
3519         UILabel *left  = [set objectAtIndex: i++];
3520         NSView  *mid   = [set objectAtIndex: i++];
3521         UILabel *right = [set objectAtIndex: i++];
3522         NSAssert (!top || [top   isKindOfClass:[UILabel class]], @"WTF");
3523         NSAssert (        [left  isKindOfClass:[UILabel class]], @"WTF");
3524         NSAssert (       ![mid   isKindOfClass:[UILabel class]], @"WTF");
3525         NSAssert (        [right isKindOfClass:[UILabel class]], @"WTF");
3526
3527         // 3 elements: control at top of cell.
3528         // 4 elements: center the control vertically.
3529         CGRect r = [mid frame];
3530         r.size.height = 32;   // Unchangable height of the slider thumb.
3531
3532         // Center the slider between left_width and right_edge.
3533 # ifdef  LABEL_ABOVE_SLIDER
3534         r.origin.x = LEFT_MARGIN;
3535 # else
3536         r.origin.x = left_width;
3537 # endif
3538         r.origin.y = (hh - r.size.height) / 2;
3539         r.size.width = right_edge - r.origin.x;
3540         [mid setFrame:r];
3541
3542         if (top) {
3543 # ifdef LABEL_ABOVE_SLIDER
3544           // Top label goes above, flush center/top.
3545           r.origin.x = (ww - r.size.width) / 2;
3546           r.origin.y = 4;
3547           // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize.
3548           r.size = [[top text] sizeWithFont:[top font]
3549                                constrainedToSize:
3550                                  CGSizeMake (ww - LEFT_MARGIN*2, 100000)
3551                                lineBreakMode:[top lineBreakMode]];
3552 # else  // !LABEL_ABOVE_SLIDER
3553           // Label goes on the left.
3554           r.origin.x = LEFT_MARGIN;
3555           r.origin.y = 0;
3556           r.size.width  = left_width - LEFT_MARGIN;
3557           r.size.height = hh;
3558 # endif // !LABEL_ABOVE_SLIDER
3559           [top setFrame:r];
3560         }
3561
3562         // Left label goes under control, flush left/bottom.
3563         left.frame = CGRectMake([mid frame].origin.x, hh - 4,
3564                                 ww - LEFT_MARGIN*2, 100000);
3565         [left sizeToFit];
3566         r = left.frame;
3567         r.origin.y -= r.size.height;
3568         left.frame = r;
3569
3570         // Right label goes under control, flush right/bottom.
3571         right.frame =
3572           CGRectMake([mid frame].origin.x + [mid frame].size.width,
3573                      [left frame].origin.y, ww - LEFT_MARGIN*2, 1000000);
3574         [right sizeToFit];
3575         r = right.frame;
3576         r.origin.x -= r.size.width;
3577         right.frame = r;
3578
3579         // Make a box and put the labels and slider into it.
3580         r.origin.x = 0;
3581         r.origin.y = 0;
3582         r.size.width  = ww;
3583         r.size.height = hh;
3584         NSView *box = [[UIView alloc] initWithFrame:r];
3585         if (top)
3586           [box addSubview: top];
3587         [box addSubview: left];
3588         [box addSubview: right];
3589         [box addSubview: mid];
3590         [box autorelease];
3591
3592         ctl = box;
3593       }
3594       break;
3595     default:
3596       NSAssert (0, @"unhandled size");
3597     }
3598   } else {      // A single view, not a pair.
3599     CGRect r = [ctl frame];
3600     r.origin.x = LEFT_MARGIN;
3601     r.origin.y = 0;
3602     r.size.width = right_edge - r.origin.x;
3603     r.size.height = hh;
3604     [ctl setFrame:r];
3605   }
3606
3607   NSString *id = @"Cell";
3608   UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
3609   if (!cell)
3610     cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3611                                      reuseIdentifier: id]
3612              autorelease];
3613
3614   for (UIView *subview in [cell.contentView subviews])
3615     [subview removeFromSuperview];
3616   [cell.contentView addSubview: ctl];
3617   CGRect r = [ctl frame];
3618   r.origin.x = 0;
3619   r.origin.y = 0;
3620   [cell setFrame:r];
3621   cell.selectionStyle = UITableViewCellSelectionStyleNone;
3622   [cell setAccessoryType:UITableViewCellAccessoryNone];
3623
3624 # ifndef USE_PICKER_VIEW
3625   if ([ctl isKindOfClass:[RadioButton class]])
3626     [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3627 # endif // USE_PICKER_VIEW
3628
3629   return cell;
3630 }
3631 # endif  // USE_IPHONE
3632
3633
3634 /* When this object is instantiated, it parses the XML file and creates
3635    controls on itself that are hooked up to the appropriate preferences.
3636    The default size of the view is just big enough to hold them all.
3637  */
3638 - (id)initWithXML: (NSData *) xml_data
3639           options: (const XrmOptionDescRec *) _opts
3640        controller: (NSUserDefaultsController *) _prefs
3641  globalController: (NSUserDefaultsController *) _globalPrefs
3642          defaults: (NSDictionary *) _defs
3643 {
3644 # ifndef USE_IPHONE
3645   self = [super init];
3646 # else  // !USE_IPHONE
3647   self = [super initWithStyle:UITableViewStyleGrouped];
3648   self.title = [saver_name stringByAppendingString:@" Settings"];
3649 # endif // !USE_IPHONE
3650   if (! self) return 0;
3651
3652   // instance variables
3653   opts = _opts;
3654   defaultOptions = _defs;
3655   userDefaultsController   = [_prefs retain];
3656   globalDefaultsController = [_globalPrefs retain];
3657
3658   NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3659
3660   if (!xmlDoc) {
3661     NSAssert1 (0, @"XML Error: %@",
3662                [[NSString alloc] initWithData:xml_data
3663                                  encoding:NSUTF8StringEncoding]);
3664     return nil;
3665   }
3666   [xmlDoc setDelegate:self];
3667   if (! [xmlDoc parse]) {
3668     NSError *err = [xmlDoc parserError];
3669     NSAssert2 (0, @"XML Error: %@: %@",
3670                [[NSString alloc] initWithData:xml_data
3671                                  encoding:NSUTF8StringEncoding],
3672                err);
3673     return nil;
3674   }
3675
3676 # ifndef USE_IPHONE
3677   TextModeTransformer *t = [[TextModeTransformer alloc] init];
3678   [NSValueTransformer setValueTransformer:t
3679                       forName:@"TextModeTransformer"];
3680   [t release];
3681 # endif // USE_IPHONE
3682
3683   [self traverseTree];
3684   xml_root = 0;
3685
3686 # ifdef USE_IPHONE
3687   [self addResetButton];
3688 # endif
3689
3690   return self;
3691 }
3692
3693
3694 - (void) dealloc
3695 {
3696   [saver_name release];
3697   [userDefaultsController release];
3698   [globalDefaultsController release];
3699 # ifdef USE_IPHONE
3700   [controls release];
3701   [pref_keys release];
3702   [pref_ctls release];
3703 #  ifdef USE_PICKER_VIEW
3704   [picker_values release];
3705 #  endif
3706 # endif
3707   [super dealloc];
3708 }
3709
3710 @end