From http://www.jwz.org/xscreensaver/xscreensaver-5.37.tar.gz
[xscreensaver] / OSX / XScreenSaverConfigSheet.m
index 2b0cb916bf35a9e2269ab3d6a35d7f439de11863..eaa5add13cb4da1e07cf56c3f84c5763a4b5379b 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2017 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
  */
 
 #import "XScreenSaverConfigSheet.h"
+#import "Updater.h"
 
 #import "jwxyz.h"
 #import "InvertedSlider.h"
-#import <Foundation/NSXMLDocument.h>
+
+#ifdef USE_IPHONE
+# define NSView      UIView
+# define NSRect      CGRect
+# define NSSize      CGSize
+# define NSTextField UITextField
+# define NSButton    UIButton
+# define NSFont      UIFont
+# define NSStepper   UIStepper
+# define NSMenuItem  UIMenuItem
+# define NSText      UILabel
+# define minValue    minimumValue
+# define maxValue    maximumValue
+# define setMinValue setMinimumValue
+# define setMaxValue setMaximumValue
+# define LABEL       UILabel
+#else
+# define LABEL       NSTextField
+#endif // USE_IPHONE
+
+#undef LABEL_ABOVE_SLIDER
+#define USE_HTML_LABELS
+
+
+#pragma mark XML Parser
+
+/* I used to use the "NSXMLDocument" XML parser, but that doesn't exist
+   on iOS.  The "NSXMLParser" parser exists on both OSX and iOS, so I
+   converted to use that.  However, to avoid having to re-write all of
+   the old code, I faked out a halfassed implementation of the
+   "NSXMLNode" class that "NSXMLDocument" used to return.
+ */
+
+#define NSXMLNode          SimpleXMLNode
+#define NSXMLElement       SimpleXMLNode
+#define NSXMLCommentKind   SimpleXMLCommentKind
+#define NSXMLElementKind   SimpleXMLElementKind
+#define NSXMLAttributeKind SimpleXMLAttributeKind
+#define NSXMLTextKind      SimpleXMLTextKind
+
+typedef enum { SimpleXMLCommentKind,
+               SimpleXMLElementKind,
+               SimpleXMLAttributeKind,
+               SimpleXMLTextKind,
+} SimpleXMLKind;
+
+@interface SimpleXMLNode : NSObject
+{
+  SimpleXMLKind kind;
+  NSString *name;
+  SimpleXMLNode *parent;
+  NSMutableArray *children;
+  NSMutableArray *attributes;
+  id object;
+}
+
+@property(nonatomic) SimpleXMLKind kind;
+@property(nonatomic, retain) NSString *name;
+@property(nonatomic, retain) SimpleXMLNode *parent;
+@property(nonatomic, retain) NSMutableArray *children;
+@property(nonatomic, retain) NSMutableArray *attributes;
+@property(nonatomic, retain, getter=objectValue, setter=setObjectValue:)
+  id object;
+
+@end
+
+@implementation SimpleXMLNode
+
+@synthesize kind;
+@synthesize name;
+//@synthesize parent;
+@synthesize children;
+@synthesize attributes;
+@synthesize object;
+
+- (id) init
+{
+  self = [super init];
+  attributes = [NSMutableArray arrayWithCapacity:10];
+  return self;
+}
+
+
+- (id) initWithName:(NSString *)n
+{
+  self = [self init];
+  [self setKind:NSXMLElementKind];
+  [self setName:n];
+  return self;
+}
+
+
+- (void) setAttributesAsDictionary:(NSDictionary *)dict
+{
+  for (NSString *key in dict) {
+    NSObject *val = [dict objectForKey:key];
+    SimpleXMLNode *n = [[SimpleXMLNode alloc] init];
+    [n setKind:SimpleXMLAttributeKind];
+    [n setName:key];
+    [n setObjectValue:val];
+    [attributes addObject:n];
+    [n release];
+  }
+}
+
+- (SimpleXMLNode *) parent { return parent; }
+
+- (void) setParent:(SimpleXMLNode *)p
+{
+  NSAssert (!parent, @"parent already set");
+  if (!p) return;
+  parent = p;
+  NSMutableArray *kids = [p children];
+  if (!kids) {
+    kids = [NSMutableArray arrayWithCapacity:10];
+    [p setChildren:kids];
+  }
+  [kids addObject:self];
+}
+@end
+
+
+#pragma mark textMode value transformer
+
+// A value transformer for mapping "url" to "3" and vice versa in the
+// "textMode" preference, since NSMatrix uses NSInteger selectedIndex.
+
+#ifndef USE_IPHONE
+@interface TextModeTransformer: NSValueTransformer {}
+@end
+@implementation TextModeTransformer
++ (Class)transformedValueClass { return [NSString class]; }
++ (BOOL)allowsReverseTransformation { return YES; }
+
+- (id)transformedValue:(id)value {
+  if ([value isKindOfClass:[NSString class]]) {
+    int i = -1;
+    if      ([value isEqualToString:@"date"])    { i = 0; }
+    else if ([value isEqualToString:@"literal"]) { i = 1; }
+    else if ([value isEqualToString:@"file"])    { i = 2; }
+    else if ([value isEqualToString:@"url"])     { i = 3; }
+    else if ([value isEqualToString:@"program"]) { i = 4; }
+    if (i != -1)
+      value = [NSNumber numberWithInt: i];
+  }
+  return value;
+}
+
+- (id)reverseTransformedValue:(id)value {
+  if ([value isKindOfClass:[NSNumber class]]) {
+    switch ((int) [value doubleValue]) {
+    case 0: value = @"date";    break;
+    case 1: value = @"literal"; break;
+    case 2: value = @"file";    break;
+    case 3: value = @"url";     break;
+    case 4: value = @"program"; break;
+    }
+  }
+  return value;
+}
+@end
+#endif // USE_IPHONE
+
+
+#pragma mark Implementing radio buttons
+
+/* The UIPickerView is a hideous and uncustomizable piece of shit.
+   I can't believe Apple actually released that thing on the world.
+   Let's fake up some radio buttons instead.
+ */
+
+#if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
+
+@interface RadioButton : UILabel
+{
+  int index;
+  NSArray *items;
+}
+
+@property(nonatomic) int index;
+@property(nonatomic, retain) NSArray *items;
+
+@end
+
+@implementation RadioButton
+
+@synthesize index;
+@synthesize items;
+
+- (id) initWithIndex:(int)_index items:_items
+{
+  self = [super initWithFrame:CGRectZero];
+  index = _index;
+  items = [_items retain];
+
+  [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
+  [self setBackgroundColor:[UIColor clearColor]];
+  [self sizeToFit];
+
+  return self;
+}
+
+@end
+
+
+# endif // !USE_PICKER_VIEW
+
+
+# pragma mark Implementing labels with clickable links
+
+#if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
+
+@interface HTMLLabel : UIView <UIWebViewDelegate>
+{
+  NSString *html;
+  UIFont *font;
+  UIWebView *webView;
+}
+
+@property(nonatomic, retain) NSString *html;
+@property(nonatomic, retain) UIWebView *webView;
+
+- (id) initWithHTML:(NSString *)h font:(UIFont *)f;
+- (id) initWithText:(NSString *)t font:(UIFont *)f;
+- (void) setHTML:(NSString *)h;
+- (void) setText:(NSString *)t;
+- (void) sizeToFit;
+
+@end
+
+@implementation HTMLLabel
+
+@synthesize html;
+@synthesize webView;
+
+- (id) initWithHTML:(NSString *)h font:(UIFont *)f
+{
+  self = [super init];
+  if (! self) return 0;
+  font = [f retain];
+  webView = [[UIWebView alloc] init];
+  webView.delegate = self;
+  webView.dataDetectorTypes = UIDataDetectorTypeNone;
+  self.   autoresizingMask = UIViewAutoresizingNone;  // we do it manually
+  webView.autoresizingMask = UIViewAutoresizingNone;
+  webView.scrollView.scrollEnabled = NO; 
+  webView.scrollView.bounces = NO;
+  webView.opaque = NO;
+  [webView setBackgroundColor:[UIColor clearColor]];
+
+  [self addSubview: webView];
+  [self setHTML: h];
+  return self;
+}
+
+- (id) initWithText:(NSString *)t font:(UIFont *)f
+{
+  self = [self initWithHTML:@"" font:f];
+  if (! self) return 0;
+  [self setText: t];
+  return self;
+}
+
+
+- (void) setHTML: (NSString *)h
+{
+  if (! h) return;
+  [h retain];
+  if (html) [html release];
+  html = h;
+  NSString *h2 =
+    [NSString stringWithFormat:
+                @"<!DOCTYPE HTML PUBLIC "
+                   "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
+                   " \"http://www.w3.org/TR/html4/loose.dtd\">"
+                 "<HTML>"
+                  "<HEAD>"
+//                   "<META NAME=\"viewport\" CONTENT=\""
+//                      "width=device-width"
+//                      "initial-scale=1.0;"
+//                      "maximum-scale=1.0;\">"
+                   "<STYLE>"
+                    "<!--\n"
+                      "body {"
+                      " margin: 0; padding: 0; border: 0;"
+                      " font-family: \"%@\";"
+                      " font-size: %.4fpx;"    // Must be "px", not "pt"!
+                      " line-height: %.4fpx;"   // And no spaces before it.
+                      " -webkit-text-size-adjust: none;"
+                      "}"
+                    "\n//-->\n"
+                   "</STYLE>"
+                  "</HEAD>"
+                  "<BODY>"
+                   "%@"
+                  "</BODY>"
+                 "</HTML>",
+              [font fontName],
+              [font pointSize],
+              [font lineHeight],
+              h];
+  [webView stopLoading];
+  [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
+}
+
+
+static char *anchorize (const char *url);
+
+- (void) setText: (NSString *)t
+{
+  t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
+                                           whitespaceCharacterSet]];
+  t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&amp;"];
+  t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"];
+  t = [t stringByReplacingOccurrencesOfString:@">" withString:@"&gt;"];
+  t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
+  t = [t stringByReplacingOccurrencesOfString:@"<P>  "
+         withString:@"<P> &nbsp; &nbsp; &nbsp; &nbsp; "];
+  t = [t stringByReplacingOccurrencesOfString:@"\n "
+         withString:@"<BR> &nbsp; &nbsp; &nbsp; &nbsp; "];
+
+  NSString *h = @"";
+  for (NSString *s in
+         [t componentsSeparatedByCharactersInSet:
+              [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
+    if ([s hasPrefix:@"http://"] ||
+        [s hasPrefix:@"https://"]) {
+      char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
+      NSString *a2 = [NSString stringWithCString: anchor
+                               encoding: NSUTF8StringEncoding];
+      s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
+      free (anchor);
+    }
+    h = [NSString stringWithFormat: @"%@ %@", h, s];
+  }
+
+  h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"];
+  h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"];
+  h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
+                                           whitespaceAndNewlineCharacterSet]];
+
+  [self setHTML: h];
+}
+
+
+-(BOOL) webView:(UIWebView *)wv
+        shouldStartLoadWithRequest:(NSURLRequest *)req
+        navigationType:(UIWebViewNavigationType)type
+{
+  // Force clicked links to open in Safari, not in this window.
+  if (type == UIWebViewNavigationTypeLinkClicked) {
+    [[UIApplication sharedApplication] openURL:[req URL]];
+    return NO;
+  }
+  return YES;
+}
+
+
+- (void) setFrame: (CGRect)r
+{
+  [super setFrame: r];
+  r.origin.x = 0;
+  r.origin.y = 0;
+  [webView setFrame: r];
+}
+
+
+- (NSString *) stripTags:(NSString *)str
+{
+  NSString *result = @"";
+
+  // Add newlines.
+  str = [str stringByReplacingOccurrencesOfString:@"<P>"
+             withString:@"<BR><BR>"
+             options:NSCaseInsensitiveSearch
+             range:NSMakeRange(0, [str length])];
+  str = [str stringByReplacingOccurrencesOfString:@"<BR>"
+             withString:@"\n"
+             options:NSCaseInsensitiveSearch
+             range:NSMakeRange(0, [str length])];
+
+  // Remove HREFs.
+  for (NSString *s in [str componentsSeparatedByString: @"<"]) {
+    NSRange r = [s rangeOfString:@">"];
+    if (r.length > 0)
+      s = [s substringFromIndex: r.location + r.length];
+    result = [result stringByAppendingString: s];
+  }
+
+  // Compress internal horizontal whitespace.
+  str = result;
+  result = @"";
+  for (NSString *s in [str componentsSeparatedByCharactersInSet:
+                             [NSCharacterSet whitespaceCharacterSet]]) {
+    if ([result length] == 0)
+      result = s;
+    else if ([s length] > 0)
+      result = [NSString stringWithFormat: @"%@ %@", result, s];
+  }
+
+  return result;
+}
+
+
+- (void) sizeToFit
+{
+  CGRect r = [self frame];
+
+  /* It would be sensible to just ask the UIWebView how tall the page is,
+     instead of hoping that NSString and UIWebView measure fonts and do
+     wrapping in exactly the same way, but since UIWebView is asynchronous,
+     we'd have to wait for the document to load first, e.g.:
+
+       - Start the document loading;
+       - return a default height to use for the UITableViewCell;
+       - wait for the webViewDidFinishLoad delegate method to fire;
+       - then force the UITableView to reload, to pick up the new height.
+
+     But I couldn't make that work.
+   */
+# if 0
+  r.size.height = [[webView
+                     stringByEvaluatingJavaScriptFromString:
+                       @"document.body.offsetHeight"]
+                    doubleValue];
+# else
+  NSString *text = [self stripTags: html];
+  CGSize s = r.size;
+  s.height = 999999;
+  s = [text boundingRectWithSize:s
+                         options:NSStringDrawingUsesLineFragmentOrigin
+                      attributes:@{NSFontAttributeName: font}
+                         context:nil].size;
+  r.size.height = s.height;
+# endif
+
+  [self setFrame: r];
+}
+
+
+- (void) dealloc
+{
+  [html release];
+  [font release];
+  [webView release];
+  [super dealloc];
+}
+
+@end
+
+#endif // USE_IPHONE && USE_HTML_LABELS
+
+
+@interface XScreenSaverConfigSheet (Private)
+
+- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
+
+# ifndef USE_IPHONE
+- (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
+- (void) placeChild: (NSView *)c on:(NSView *)p;
+static NSView *last_child (NSView *parent);
+static void layout_group (NSView *group, BOOL horiz_p);
+# else // USE_IPHONE
+- (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
+- (void) placeChild: (NSObject *)c on:(NSView *)p;
+- (void) placeSeparator;
+- (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
+- (void) refreshTableView;
+# endif // USE_IPHONE
+
+@end
+
 
 @implementation XScreenSaverConfigSheet
 
-#define LEFT_MARGIN       20   // left edge of window
-#define COLUMN_SPACING    10   // gap between e.g. labels and text fields
-#define LEFT_LABEL_WIDTH  70   // width of all left labels
-#define LINE_SPACING      10   // leading between each line
+# define LEFT_MARGIN      20   // left edge of window
+# define COLUMN_SPACING   10   // gap between e.g. labels and text fields
+# define LEFT_LABEL_WIDTH 70   // width of all left labels
+# define LINE_SPACING     10   // leading between each line
+
+# define FONT_SIZE       17   // Magic hardcoded UITableView font size.
 
-// redefine these since they don't work when not inside an ObjC method
-#undef NSAssert
-#undef NSAssert1
-#undef NSAssert2
-#undef NSAssert3
-#define NSAssert(CC,S)        do { if (!(CC)) { NSLog(S);       }} while(0)
-#define NSAssert1(CC,S,A)     do { if (!(CC)) { NSLog(S,A);     }} while(0)
-#define NSAssert2(CC,S,A,B)   do { if (!(CC)) { NSLog(S,A,B);   }} while(0)
-#define NSAssert3(CC,S,A,B,C) do { if (!(CC)) { NSLog(S,A,B,C); }} while(0)
+#pragma mark Talking to the resource database
+
+
+/* Normally we read resources by looking up "KEY" in the database
+   "org.jwz.xscreensaver.SAVERNAME".  But in the all-in-one iPhone
+   app, everything is stored in the database "org.jwz.xscreensaver"
+   instead, so transform keys to "SAVERNAME.KEY".
+
+   NOTE: This is duplicated in PrefsReader.m, cause I suck.
+ */
+- (NSString *) makeKey:(NSString *)key
+{
+# ifdef USE_IPHONE
+  NSString *prefix = [saver_name stringByAppendingString:@"."];
+  if (! [key hasPrefix:prefix])  // Don't double up!
+    key = [prefix stringByAppendingString:key];
+# endif
+  return key;
+}
+
+
+- (NSString *) makeCKey:(const char *)key
+{
+  return [self makeKey:[NSString stringWithCString:key
+                                 encoding:NSUTF8StringEncoding]];
+}
 
 
 /* Given a command-line option, returns the corresponding resource name.
    Any arguments in the switch string are ignored (e.g., "-foo x").
  */
-static NSString *
-switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts,
-                    NSString **val_ret)
+- (NSString *) switchToResource:(NSString *)cmdline_switch
+                           opts:(const XrmOptionDescRec *)opts_array
+                         valRet:(NSString **)val_ret
 {
-  char buf[255];
+  char buf[1280];
   char *tail = 0;
   NSAssert(cmdline_switch, @"cmdline switch is null");
   if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
                           encoding:NSUTF8StringEncoding]) {
     NSAssert1(0, @"unable to convert %@", cmdline_switch);
-    abort();
+    return 0;
   }
   char *s = strpbrk(buf, " \t\r\n");
   if (s && *s) {
@@ -70,15 +561,15 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts,
       tail++;
   }
   
-  while (opts[0].option) {
-    if (!strcmp (opts[0].option, buf)) {
+  while (opts_array[0].option) {
+    if (!strcmp (opts_array[0].option, buf)) {
       const char *ret = 0;
 
-      if (opts[0].argKind == XrmoptionNoArg) {
+      if (opts_array[0].argKind == XrmoptionNoArg) {
         if (tail && *tail)
           NSAssert1 (0, @"expected no args to switch: \"%@\"",
                      cmdline_switch);
-        ret = opts[0].value;
+        ret = opts_array[0].value;
       } else {
         if (!tail || !*tail)
           NSAssert1 (0, @"expected args to switch: \"%@\"",
@@ -92,57 +583,555 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts,
                                          encoding:NSUTF8StringEncoding]
                     : 0);
       
-      const char *res = opts[0].specifier;
+      const char *res = opts_array[0].specifier;
       while (*res && (*res == '.' || *res == '*'))
         res++;
-      return [NSString stringWithCString:res
-                                encoding:NSUTF8StringEncoding];
+      return [self makeCKey:res];
     }
-    opts++;
+    opts_array++;
   }
   
   NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
-  abort();
+  return 0;
+}
+
+
+- (NSUserDefaultsController *)controllerForKey:(NSString *)key
+{
+  static NSDictionary *a = 0;
+  if (! a) {
+    a = UPDATER_DEFAULTS;
+    [a retain];
+  }
+  if ([a objectForKey:key])
+    // These preferences are global to all xscreensavers.
+    return globalDefaultsController;
+  else
+    // All other preferences are per-saver.
+    return userDefaultsController;
+}
+
+
+#ifdef USE_IPHONE
+
+// Called when a slider is bonked.
+//
+- (void)sliderAction:(UISlider*)sender
+{
+  if ([active_text_field canResignFirstResponder])
+    [active_text_field resignFirstResponder];
+  NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
+
+  // Hacky API. See comment in InvertedSlider.m.
+  double v = ([sender isKindOfClass: [InvertedSlider class]]
+              ? [(InvertedSlider *) sender transformedValue]
+              : [sender value]);
+
+  [[self controllerForKey:pref_key]
+    setObject:((v == (int) v)
+               ? [NSNumber numberWithInt:(int) v]
+               : [NSNumber numberWithDouble: v])
+    forKey:pref_key];
+}
+
+// Called when a checkbox/switch is bonked.
+//
+- (void)switchAction:(UISwitch*)sender
+{
+  if ([active_text_field canResignFirstResponder])
+    [active_text_field resignFirstResponder];
+  NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
+  NSString *v = ([sender isOn] ? @"true" : @"false");
+  [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
+}
+
+# ifdef USE_PICKER_VIEW
+// Called when a picker is bonked.
+//
+- (void)pickerView:(UIPickerView *)pv
+        didSelectRow:(NSInteger)row
+        inComponent:(NSInteger)column
+{
+  if ([active_text_field canResignFirstResponder])
+    [active_text_field resignFirstResponder];
+
+  NSAssert (column == 0, @"internal error");
+  NSArray *a = [picker_values objectAtIndex: [pv tag]];
+  if (! a) return;  // Too early?
+  a = [a objectAtIndex:row];
+  NSAssert (a, @"missing row");
+
+//NSString *label    = [a objectAtIndex:0];
+  NSString *pref_key = [a objectAtIndex:1];
+  NSObject *pref_val = [a objectAtIndex:2];
+  [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
+}
+# else  // !USE_PICKER_VIEW
+
+// Called when a RadioButton is bonked.
+//
+- (void)radioAction:(RadioButton*)sender
+{
+  if ([active_text_field canResignFirstResponder])
+    [active_text_field resignFirstResponder];
+
+  NSArray *item = [[sender items] objectAtIndex: [sender index]];
+  NSString *pref_key = [item objectAtIndex:1];
+  NSObject *pref_val = [item objectAtIndex:2];
+  [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
+}
+
+- (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
+{
+  active_text_field = tf;
+  return YES;
+}
+
+- (void)textFieldDidEndEditing:(UITextField *)tf
+{
+  NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
+  NSString *txt = [tf text];
+  [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
+}
+
+- (BOOL)textFieldShouldReturn:(UITextField *)tf
+{
+  active_text_field = nil;
+  [tf resignFirstResponder];
+  return YES;
+}
+
+# endif // !USE_PICKER_VIEW
+
+#endif // USE_IPHONE
+
+
+# ifndef USE_IPHONE
+
+- (void) okAction:(NSObject *)arg
+{
+  // Without the setAppliesImmediately:, when the saver restarts, it's still
+  // got the old settings. -[XScreenSaverConfigSheet traverseTree] sets this
+  // to NO; default is YES.
+
+  // #### However: I'm told that when these are set to YES, then changes to
+  // 'textLiteral', 'textURL' and 'textProgram' are ignored, but 'textFile'
+  // works.  In StarWars, at least...
+
+  [userDefaultsController   setAppliesImmediately:YES];
+  [globalDefaultsController setAppliesImmediately:YES];
+  [userDefaultsController   commitEditing];
+  [globalDefaultsController commitEditing];
+  [userDefaultsController   save:self];
+  [globalDefaultsController save:self];
+  [NSApp endSheet:self returnCode:NSOKButton];
+  [self close];
+}
+
+- (void) cancelAction:(NSObject *)arg
+{
+  [userDefaultsController   revert:self];
+  [globalDefaultsController revert:self];
+  [NSApp endSheet:self returnCode:NSCancelButton];
+  [self close];
+}
+# endif // !USE_IPHONE
+
+
+- (void) resetAction:(NSObject *)arg
+{
+# ifndef USE_IPHONE
+  [userDefaultsController   revertToInitialValues:self];
+  [globalDefaultsController revertToInitialValues:self];
+# else  // USE_IPHONE
+
+  for (NSString *key in defaultOptions) {
+    NSObject *val = [defaultOptions objectForKey:key];
+    [[self controllerForKey:key] setObject:val forKey:key];
+  }
+
+  for (UIControl *ctl in pref_ctls) {
+    NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
+    [self bindResource:ctl key:pref_key reload:YES];
+  }
+
+  [self refreshTableView];
+# endif // USE_IPHONE
 }
 
 
 /* Connects a control (checkbox, etc) to the corresponding preferences key.
  */
-static void
-bind_resource_to_preferences (NSUserDefaultsController *prefs,
-                              NSObject *control, 
-                              NSString *pref_key,
-                              const XrmOptionDescRec *opts)
+- (void) bindResource:(NSObject *)control key:(NSString *)pref_key
+         reload:(BOOL)reload_p
 {
+  NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
+# ifndef USE_IPHONE
+  NSDictionary *opts_dict = nil;
   NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
                       ? @"selectedObject"
                       : ([control isKindOfClass:[NSMatrix class]]
                          ? @"selectedIndex"
                          : @"value"));
+
+  if ([control isKindOfClass:[NSMatrix class]]) {
+    opts_dict = @{ NSValueTransformerNameBindingOption:
+                   @"TextModeTransformer" };
+  }
+
   [control bind:bindto
        toObject:prefs
     withKeyPath:[@"values." stringByAppendingString: pref_key]
-        options:nil];
+        options:opts_dict];
+
+# else  // USE_IPHONE
+  SEL sel;
+  NSObject *val = [prefs objectForKey:pref_key];
+  NSString *sval = 0;
+  double dval = 0;
+
+  if ([val isKindOfClass:[NSString class]]) {
+    sval = (NSString *) val;
+    if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
+        NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
+        NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
+      dval = 1;
+    else
+      dval = [sval doubleValue];
+  } else if ([val isKindOfClass:[NSNumber class]]) {
+    // NSBoolean (__NSCFBoolean) is really NSNumber.
+    dval = [(NSNumber *) val doubleValue];
+    sval = [(NSNumber *) val stringValue];
+  }
 
-# if 0 // ####
+  if ([control isKindOfClass:[UISlider class]]) {
+    sel = @selector(sliderAction:);
+    // Hacky API. See comment in InvertedSlider.m.
+    if ([control isKindOfClass:[InvertedSlider class]])
+      [(InvertedSlider *) control setTransformedValue: dval];
+    else
+      [(UISlider *) control setValue: dval];
+  } else if ([control isKindOfClass:[UISwitch class]]) {
+    sel = @selector(switchAction:);
+    [(UISwitch *) control setOn: ((int) dval != 0)];
+# ifdef USE_PICKER_VIEW
+  } else if ([control isKindOfClass:[UIPickerView class]]) {
+    sel = 0;
+    [(UIPickerView *) control selectRow:((int)dval) inComponent:0
+                      animated:NO];
+# else  // !USE_PICKER_VIEW
+  } else if ([control isKindOfClass:[RadioButton class]]) {
+    sel = 0;  // radioAction: sent from didSelectRowAtIndexPath.
+  } else if ([control isKindOfClass:[UITextField class]]) {
+    sel = 0;  // ####
+    [(UITextField *) control setText: sval];
+# endif // !USE_PICKER_VIEW
+  } else {
+    NSAssert (0, @"unknown class");
+  }
+
+  // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
+
+  if (!reload_p) {
+    if (! pref_keys) {
+      pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
+      pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
+    }
+
+    [pref_keys addObject: [self makeKey:pref_key]];
+    [pref_ctls addObject: control];
+    ((UIControl *) control).tag = [pref_keys count] - 1;
+
+    if (sel) {
+      [(UIControl *) control addTarget:self action:sel
+                     forControlEvents:UIControlEventValueChanged];
+    }
+  }
+
+# endif // USE_IPHONE
+
+# if 0
   NSObject *def = [[prefs defaults] objectForKey:pref_key];
   NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
   s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
-  s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
-  s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
-  NSLog (@"%@ %@/%@", s, [def class], [control class]);
+  s = [NSString stringWithFormat:@"%@ = %@", s, 
+                ([def isKindOfClass:[NSString class]]
+                 ? [NSString stringWithFormat:@"\"%@\"", def]
+                 : def)];
+  s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0];
+  s = [NSString stringWithFormat:@"%@ %@ / %@", s,
+                [def class], [control class]];
+#  ifndef USE_IPHONE
+  s = [NSString stringWithFormat:@"%@ / %@", s, bindto];
+#  endif
+  NSLog (@"%@", s);
+# endif
+}
+
+
+- (void) bindResource:(NSObject *)control key:(NSString *)pref_key
+{
+  [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
+}
+
+
+
+- (void) bindSwitch:(NSObject *)control
+            cmdline:(NSString *)cmd
+{
+  [self bindResource:control 
+        key:[self switchToResource:cmd opts:opts valRet:0]];
+}
+
+
+#pragma mark Text-manipulating utilities
+
+
+static NSString *
+unwrap (NSString *text)
+{
+  // Unwrap lines: delete \n but do not delete \n\n.
+  //
+  NSArray *lines = [text componentsSeparatedByString:@"\n"];
+  NSUInteger i, nlines = [lines count];
+  BOOL eolp = YES;
+
+  text = @"\n";      // start with one blank line
+
+  // skip trailing blank lines in file
+  for (i = nlines-1; i > 0; i--) {
+    NSString *s = (NSString *) [lines objectAtIndex:i];
+    if ([s length] > 0)
+      break;
+    nlines--;
+  }
+
+  // skip leading blank lines in file
+  for (i = 0; i < nlines; i++) {
+    NSString *s = (NSString *) [lines objectAtIndex:i];
+    if ([s length] > 0)
+      break;
+  }
+
+  // unwrap
+  Bool any = NO;
+  for (; i < nlines; i++) {
+    NSString *s = (NSString *) [lines objectAtIndex:i];
+    if ([s length] == 0) {
+      text = [text stringByAppendingString:@"\n\n"];
+      eolp = YES;
+    } else if ([s characterAtIndex:0] == ' ' ||
+               [s hasPrefix:@"Copyright "] ||
+               [s hasPrefix:@"https://"] ||
+               [s hasPrefix:@"http://"]) {
+      // don't unwrap if the following line begins with whitespace,
+      // or with the word "Copyright", or if it begins with a URL.
+      if (any && !eolp)
+        text = [text stringByAppendingString:@"\n"];
+      text = [text stringByAppendingString:s];
+      any = YES;
+      eolp = NO;
+    } else {
+      if (!eolp)
+        text = [text stringByAppendingString:@" "];
+      text = [text stringByAppendingString:s];
+      eolp = NO;
+      any = YES;
+    }
+  }
+
+  return text;
+}
+
+
+# ifndef USE_IPHONE
+/* Makes the text up to the first comma be bold.
+ */
+static void
+boldify (NSText *nstext)
+{
+  NSString *text = [nstext string];
+  NSRange r = [text rangeOfString:@"," options:0];
+  r.length = r.location+1;
+
+  r.location = 0;
+
+  NSFont *font = [nstext font];
+  font = [NSFont boldSystemFontOfSize:[font pointSize]];
+  [nstext setFont:font range:r];
+}
+# endif // !USE_IPHONE
+
+
+/* Creates a human-readable anchor to put on a URL.
+ */
+static char *
+anchorize (const char *url)
+{
+  const char *wiki1 =  "http://en.wikipedia.org/wiki/";
+  const char *wiki2 = "https://en.wikipedia.org/wiki/";
+  const char *math1 =  "http://mathworld.wolfram.com/";
+  const char *math2 = "https://mathworld.wolfram.com/";
+  if (!strncmp (wiki1, url, strlen(wiki1)) ||
+      !strncmp (wiki2, url, strlen(wiki2))) {
+    char *anchor = (char *) malloc (strlen(url) * 3 + 10);
+    strcpy (anchor, "Wikipedia: \"");
+    const char *in = url + strlen(!strncmp (wiki1, url, strlen(wiki1))
+                                  ? wiki1 : wiki2);
+    char *out = anchor + strlen(anchor);
+    while (*in) {
+      if (*in == '_') {
+        *out++ = ' ';
+      } else if (*in == '#') {
+        *out++ = ':';
+        *out++ = ' ';
+      } else if (*in == '%') {
+        char hex[3];
+        hex[0] = in[1];
+        hex[1] = in[2];
+        hex[2] = 0;
+        int n = 0;
+        sscanf (hex, "%x", &n);
+        *out++ = (char) n;
+        in += 2;
+      } else {
+        *out++ = *in;
+      }
+      in++;
+    }
+    *out++ = '"';
+    *out = 0;
+    return anchor;
+
+  } else if (!strncmp (math1, url, strlen(math1)) ||
+             !strncmp (math2, url, strlen(math2))) {
+    char *anchor = (char *) malloc (strlen(url) * 3 + 10);
+    strcpy (anchor, "MathWorld: \"");
+    const char *start = url + strlen(!strncmp (math1, url, strlen(math1))
+                                     ? math1 : math2);
+    const char *in = start;
+    char *out = anchor + strlen(anchor);
+    while (*in) {
+      if (*in == '_') {
+        *out++ = ' ';
+      } else if (in != start && *in >= 'A' && *in <= 'Z') {
+        *out++ = ' ';
+        *out++ = *in;
+      } else if (!strncmp (in, ".htm", 4)) {
+        break;
+      } else {
+        *out++ = *in;
+      }
+      in++;
+    }
+    *out++ = '"';
+    *out = 0;
+    return anchor;
+
+  } else {
+    return strdup (url);
+  }
+}
+
+
+#if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
+
+/* Converts any http: URLs in the given text field to clickable links.
+ */
+static void
+hreffify (NSText *nstext)
+{
+# ifndef USE_IPHONE
+  NSString *text = [nstext string];
+  [nstext setRichText:YES];
+# else
+  NSString *text = [nstext text];
+# endif
+
+  NSUInteger L = [text length];
+  NSRange start;               // range is start-of-search to end-of-string
+  start.location = 0;
+  start.length = L;
+  while (start.location < L) {
+
+    // Find the beginning of a URL...
+    //
+    NSRange r2 = [text rangeOfString: @"http://" options:0 range:start];
+    NSRange r3 = [text rangeOfString:@"https://" options:0 range:start];
+    if ((r2.location == NSNotFound &&
+         r3.location != NSNotFound) ||
+        (r2.location != NSNotFound &&
+         r3.location != NSNotFound &&
+         r3.location < r2.location))
+      r2 = r3;
+    if (r2.location == NSNotFound)
+      break;
+
+    // Next time around, start searching after this.
+    start.location = r2.location + r2.length;
+    start.length = L - start.location;
+
+    // Find the end of a URL (whitespace or EOF)...
+    //
+    r3 = [text rangeOfCharacterFromSet:
+                 [NSCharacterSet whitespaceAndNewlineCharacterSet]
+               options:0 range:start];
+    if (r3.location == NSNotFound)    // EOF
+      r3.location = L, r3.length = 0;
+
+    // Next time around, start searching after this.
+    start.location = r3.location;
+    start.length = L - start.location;
+
+    // Set r2 to the start/length of this URL.
+    r2.length = start.location - r2.location;
+
+    // Extract the URL.
+    NSString *nsurl = [text substringWithRange:r2];
+    const char *url = [nsurl UTF8String];
+
+    // If this is a Wikipedia URL, make the linked text be prettier.
+    //
+    char *anchor = anchorize(url);
+
+# ifndef USE_IPHONE
+
+    // Construct the RTF corresponding to <A HREF="url">anchor</A>
+    //
+    const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
+    char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
+    sprintf (rtf, fmt, url, anchor);
+
+    NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
+    [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
+
+# else  // !USE_IPHONE
+    // *anchor = 0; // Omit Wikipedia anchor 
+    text = [text stringByReplacingCharactersInRange:r2
+                 withString:[NSString stringWithCString:anchor
+                                      encoding:NSUTF8StringEncoding]];
+    // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
+    //              withString:@"\n\n"];
+# endif // !USE_IPHONE
+
+    free (anchor);
+
+    NSUInteger L2 = [text length];  // might have changed
+    start.location -= (L - L2);
+    L = L2;
+  }
+
+# ifdef USE_IPHONE
+  [nstext setText:text];
+  [nstext sizeToFit];
 # endif
 }
 
-static void
-bind_switch_to_preferences (NSUserDefaultsController *prefs,
-                            NSObject *control, 
-                            NSString *cmdline_switch,
-                            const XrmOptionDescRec *opts)
-{
-  NSString *pref_key = switch_to_resource (cmdline_switch, opts, 0);
-  bind_resource_to_preferences (prefs, control, pref_key, opts);
-}
+#endif /* !USE_IPHONE || !USE_HTML_LABELS */
+
+
+
+#pragma mark Creating controls from XML
 
 
 /* Parse the attributes of an XML tag into a dictionary.
@@ -152,11 +1141,10 @@ bind_switch_to_preferences (NSUserDefaultsController *prefs,
    and keys that were not specified will not be present in the dictionary.
    Warnings are printed if there are duplicate or unknown attributes.
  */
-static void
-parse_attrs (NSMutableDictionary *dict, NSXMLNode *node)
+- (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
 {
   NSArray *attrs = [(NSXMLElement *) node attributes];
-  int n = [attrs count];
+  NSUInteger n = [attrs count];
   int i;
   
   // For each key in the dictionary, fill in the dict with the corresponding
@@ -189,17 +1177,57 @@ parse_attrs (NSMutableDictionary *dict, NSXMLNode *node)
     if ([val length] == 0)
       [dict removeObjectForKey:key];
   }
+
+# ifdef USE_IPHONE
+  // Kludge for starwars.xml:
+  // If there is a "_low-label" and no "_label", but "_low-label" contains
+  // spaces, divide them.
+  NSString *lab = [dict objectForKey:@"_label"];
+  NSString *low = [dict objectForKey:@"_low-label"];
+  if (low && !lab) {
+    NSArray *split =
+      [[[low stringByTrimmingCharactersInSet:
+               [NSCharacterSet whitespaceAndNewlineCharacterSet]]
+         componentsSeparatedByString: @"  "]
+        filteredArrayUsingPredicate:
+          [NSPredicate predicateWithFormat:@"length > 0"]];
+    if (split && [split count] == 2) {
+      [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
+      [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
+    }
+  }
+# endif // USE_IPHONE
+}
+
+
+/* Handle the options on the top level <xscreensaver> tag.
+ */
+- (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
+{
+  NSMutableDictionary *dict = [@{ @"name":   @"",
+                                  @"_label": @"",
+                                  @"gl":     @"" }
+                                mutableCopy];
+  [self parseAttrs:dict node:node];
+  NSString *name  = [dict objectForKey:@"name"];
+  NSString *label = [dict objectForKey:@"_label"];
+  [dict release];
+  dict = 0;
+    
+  NSAssert1 (label, @"no _label in %@", [node name]);
+  NSAssert1 (name, @"no name in \"%@\"", label);
+  return label;
 }
 
 
 /* Creates a label: an un-editable NSTextField displaying the given text.
  */
-static NSTextField *
-make_label (NSString *text)
+- (LABEL *) makeLabel:(NSString *)text
 {
   NSRect rect;
   rect.origin.x = rect.origin.y = 0;
   rect.size.width = rect.size.height = 10;
+# ifndef USE_IPHONE
   NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
   [lab setSelectable:NO];
   [lab setEditable:NO];
@@ -207,69 +1235,37 @@ make_label (NSString *text)
   [lab setDrawsBackground:NO];
   [lab setStringValue:text];
   [lab sizeToFit];
+# else  // USE_IPHONE
+  UILabel *lab = [[UILabel alloc] initWithFrame:rect];
+  [lab setText: [text stringByTrimmingCharactersInSet:
+                 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
+  [lab setBackgroundColor:[UIColor clearColor]];
+  [lab setNumberOfLines:0]; // unlimited
+  // [lab setLineBreakMode:UILineBreakModeWordWrap];
+  [lab setLineBreakMode:NSLineBreakByTruncatingHead];
+  [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
+                             UIViewAutoresizingFlexibleHeight)];
+# endif // USE_IPHONE
+  [lab autorelease];
   return lab;
 }
 
 
-static NSView *
-last_child (NSView *parent)
-{
-  NSArray *kids = [parent subviews];
-  int nkids = [kids count];
-  if (nkids == 0)
-    return 0;
-  else
-    return [kids objectAtIndex:nkids-1];
-}
-
-
-/* Add the child as a subview of the parent, positioning it immediately
-   below or to the right of the previously-added child of that view.
- */
-static void
-place_child (NSView *parent, NSView *child, BOOL right_p)
-{
-  NSRect rect = [child frame];
-  NSView *last = last_child (parent);
-  if (!last) {
-    rect.origin.x = LEFT_MARGIN;
-    rect.origin.y = [parent frame].size.height - rect.size.height 
-      - LINE_SPACING;
-  } else if (right_p) {
-    rect = [last frame];
-    rect.origin.x += rect.size.width + COLUMN_SPACING;
-  } else {
-    rect = [last frame];
-    rect.origin.x = LEFT_MARGIN;
-    rect.origin.y -= [child frame].size.height + LINE_SPACING;
-  }
-  [child setFrameOrigin:rect.origin];
-  [parent addSubview:child];
-}
-
-
-static void traverse_children (NSUserDefaultsController *,
-                               const XrmOptionDescRec *, 
-                               NSView *, NSXMLNode *);
-
-
 /* Creates the checkbox (NSButton) described by the given XML node.
  */
-static void
-make_checkbox (NSUserDefaultsController *prefs,
-               const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
-{
-  NSMutableDictionary *dict =
-    [NSMutableDictionary dictionaryWithObjectsAndKeys:
-      @"", @"id",
-      @"", @"_label",
-      @"", @"arg-set",
-      @"", @"arg-unset",
-      nil];
-  parse_attrs (dict, node);
+- (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
+{
+  NSMutableDictionary *dict = [@{ @"id":       @"",
+                                  @"_label":    @"",
+                                  @"arg-set":   @"",
+                                  @"arg-unset": @"" }
+                                mutableCopy];
+  [self parseAttrs:dict node:node];
   NSString *label     = [dict objectForKey:@"_label"];
   NSString *arg_set   = [dict objectForKey:@"arg-set"];
   NSString *arg_unset = [dict objectForKey:@"arg-unset"];
+  [dict release];
+  dict = 0;
   
   if (!label) {
     NSAssert1 (0, @"no _label in %@", [node name]);
@@ -299,286 +1295,46 @@ make_checkbox (NSUserDefaultsController *prefs,
   rect.origin.x = rect.origin.y = 0;
   rect.size.width = rect.size.height = 10;
 
+# ifndef USE_IPHONE
+
   NSButton *button = [[NSButton alloc] initWithFrame:rect];
-  [button setButtonType:([[node name] isEqualToString:@"radio"]
-                         ? NSRadioButton
-                         : NSSwitchButton)];
+  [button setButtonType:NSSwitchButton];
   [button setTitle:label];
   [button sizeToFit];
-  place_child (parent, button, NO);
-  
-  bind_switch_to_preferences (prefs, button,
-                              (arg_set ? arg_set : arg_unset),
-                              opts);
-  [button release];
-}
-
-
-/* Creates the NSTextField described by the given XML node.
-*/
-static void
-make_text_field (NSUserDefaultsController *prefs,
-                 const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node,
-                 BOOL no_label_p)
-{
-  NSMutableDictionary *dict =
-  [NSMutableDictionary dictionaryWithObjectsAndKeys:
-    @"", @"id",
-    @"", @"_label",
-    @"", @"arg",
-    nil];
-  parse_attrs (dict, node);
-  NSString *label = [dict objectForKey:@"_label"];
-  NSString *arg   = [dict objectForKey:@"arg"];
-
-  if (!label && !no_label_p) {
-    NSAssert1 (0, @"no _label in %@", [node name]);
-    return;
-  }
-
-  NSAssert1 (arg, @"no arg in %@", label);
-
-  NSRect rect;
-  rect.origin.x = rect.origin.y = 0;    
-  rect.size.width = rect.size.height = 10;
-  
-  NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
-
-  // make the default size be around 30 columns; a typical value for
-  // these text fields is "xscreensaver-text --cols 40".
-  //
-  [txt setStringValue:@"123456789 123456789 123456789 "];
-  [txt sizeToFit];
-  [[txt cell] setWraps:NO];
-  [[txt cell] setScrollable:YES];
-  [txt setStringValue:@""];
-  
-  if (label) {
-    NSTextField *lab = make_label (label);
-    place_child (parent, lab, NO);
-    [lab release];
-  }
-
-  place_child (parent, txt, (label ? YES : NO));
-
-  bind_switch_to_preferences (prefs, txt, arg, opts);
-  [txt release];
-}
+  [self placeChild:button on:parent];
 
+# else  // USE_IPHONE
 
-/* Creates the NSTextField described by the given XML node,
-   and hooks it up to a Choose button and a file selector widget.
-*/
-static void
-make_file_selector (NSUserDefaultsController *prefs,
-                    const XrmOptionDescRec *opts, 
-                    NSView *parent, NSXMLNode *node,
-                    BOOL dirs_only_p,
-                    BOOL no_label_p,
-                    BOOL editable_text_p)
-{
-  NSMutableDictionary *dict =
-  [NSMutableDictionary dictionaryWithObjectsAndKeys:
-    @"", @"id",
-    @"", @"_label",
-    @"", @"arg",
-    nil];
-  parse_attrs (dict, node);
-  NSString *label = [dict objectForKey:@"_label"];
-  NSString *arg   = [dict objectForKey:@"arg"];
-
-  if (!label && !no_label_p) {
-    NSAssert1 (0, @"no _label in %@", [node name]);
-    return;
-  }
-
-  NSAssert1 (arg, @"no arg in %@", label);
+  LABEL *lab = [self makeLabel:label];
+  [self placeChild:lab on:parent];
+  UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
+  [self placeChild:button on:parent right:YES];
 
-  NSRect rect;
-  rect.origin.x = rect.origin.y = 0;    
-  rect.size.width = rect.size.height = 10;
+# endif // USE_IPHONE
   
-  NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
-
-  // make the default size be around 20 columns.
-  //
-  [txt setStringValue:@"123456789 123456789 "];
-  [txt sizeToFit];
-  [txt setSelectable:YES];
-  [txt setEditable:editable_text_p];
-  [txt setBezeled:editable_text_p];
-  [txt setDrawsBackground:editable_text_p];
-  [[txt cell] setWraps:NO];
-  [[txt cell] setScrollable:YES];
-  [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
-  [txt setStringValue:@""];
-
-  NSTextField *lab = 0;
-  if (label) {
-    lab = make_label (label);
-    place_child (parent, lab, NO);
-    [lab release];
-  }
-
-  place_child (parent, txt, (label ? YES : NO));
-
-  bind_switch_to_preferences (prefs, txt, arg, opts);
-  [txt release];
-
-  // Make the text field and label be the same height, whichever is taller.
-  if (lab) {
-    rect = [txt frame];
-    rect.size.height = ([lab frame].size.height > [txt frame].size.height
-                        ? [lab frame].size.height
-                        : [txt frame].size.height);
-    [txt setFrame:rect];
-  }
-
-  // Now put a "Choose" button next to it.
-  //
-  rect.origin.x = rect.origin.y = 0;    
-  rect.size.width = rect.size.height = 10;
-  NSButton *choose = [[NSButton alloc] initWithFrame:rect];
-  [choose setTitle:@"Choose..."];
-  [choose setBezelStyle:NSRoundedBezelStyle];
-  [choose sizeToFit];
-
-  place_child (parent, choose, YES);
-
-  // center the Choose button around the midpoint of the text field.
-  rect = [choose frame];
-  rect.origin.y = ([txt frame].origin.y + 
-                   (([txt frame].size.height - rect.size.height) / 2));
-  [choose setFrameOrigin:rect.origin];
-
-  [choose setTarget:[parent window]];
-  if (dirs_only_p)
-    [choose setAction:@selector(chooseClickedDirs:)];
-  else
-    [choose setAction:@selector(chooseClicked:)];
-
-  [choose release];
-}
-
-
-/* Runs a modal file selector and sets the text field's value to the
-   selected file or directory.
- */
-static void
-do_file_selector (NSTextField *txt, BOOL dirs_p)
-{
-  NSOpenPanel *panel = [NSOpenPanel openPanel];
-  [panel setAllowsMultipleSelection:NO];
-  [panel setCanChooseFiles:!dirs_p];
-  [panel setCanChooseDirectories:dirs_p];
-
-  NSString *file = [txt stringValue];
-  if ([file length] <= 0) {
-    file = NSHomeDirectory();
-    if (dirs_p)
-      file = [file stringByAppendingPathComponent:@"Pictures"];
-  }
-
-//  NSString *dir = [file stringByDeletingLastPathComponent];
-
-  int result = [panel runModalForDirectory:file //dir
-                                      file:nil //[file lastPathComponent]
-                                     types:nil];
-  if (result == NSOKButton) {
-    NSArray *files = [panel filenames];
-    file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
-    file = [file stringByAbbreviatingWithTildeInPath];
-    [txt setStringValue:file];
-
-    // Fuck me!  Just setting the value of the NSTextField does not cause
-    // that to end up in the preferences!
-    //
-    NSDictionary *dict = [txt infoForBinding:@"value"];
-    NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
-    NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
-    if ([path hasPrefix:@"values."])  // WTF.
-      path = [path substringFromIndex:7];
-    [[prefs values] setValue:file forKey:path];
-
-#if 0
-    // make sure the end of the string is visible.
-    NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
-    NSRange range;
-    range.location = [file length]-3;
-    range.length = 1;
-    if (! [[txt window] makeFirstResponder:[txt window]])
-      [[txt window] endEditingFor:nil];
-//    [[txt window] makeFirstResponder:nil];
-    [fe setSelectedRange:range];
-//    [tv scrollRangeToVisible:range];
-//    [txt setNeedsDisplay:YES];
-//    [[txt cell] setNeedsDisplay:YES];
-//    [txt selectAll:txt];
-#endif
-  }
-}
-
-/* Returns the NSTextField that is to the left of or above the NSButton.
- */
-static NSTextField *
-find_text_field_of_button (NSButton *button)
-{
-  NSView *parent = [button superview];
-  NSArray *kids = [parent subviews];
-  int nkids = [kids count];
-  int i;
-  NSTextField *f = 0;
-  for (i = 0; i < nkids; i++) {
-    NSObject *kid = [kids objectAtIndex:i];
-    if ([kid isKindOfClass:[NSTextField class]]) {
-      f = (NSTextField *) kid;
-    } else if (kid == button) {
-      if (! f) abort();
-      return f;
-    }
-  }
-  abort();
-}
-
-
-- (void) chooseClicked:(NSObject *)arg
-{
-  NSButton *choose = (NSButton *) arg;
-  NSTextField *txt = find_text_field_of_button (choose);
-  do_file_selector (txt, NO);
-}
-
-- (void) chooseClickedDirs:(NSObject *)arg
-{
-  NSButton *choose = (NSButton *) arg;
-  NSTextField *txt = find_text_field_of_button (choose);
-  do_file_selector (txt, YES);
+  [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
+  [button release];
 }
 
 
 /* Creates the number selection control described by the given XML node.
    If "type=slider", it's an NSSlider.
    If "type=spinbutton", it's a text field with up/down arrows next to it.
-*/
-static void
-make_number_selector (NSUserDefaultsController *prefs,
-                      const XrmOptionDescRec *opts, 
-                      NSView *parent, NSXMLNode *node)
-{
-  NSMutableDictionary *dict =
-  [NSMutableDictionary dictionaryWithObjectsAndKeys:
-    @"", @"id",
-    @"", @"_label",
-    @"", @"_low-label",
-    @"", @"_high-label",
-    @"", @"type",
-    @"", @"arg",
-    @"", @"low",
-    @"", @"high",
-    @"", @"default",
-    @"", @"convert",
-    nil];
-  parse_attrs (dict, node);
+ */
+- (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
+{
+  NSMutableDictionary *dict = [@{ @"id":          @"",
+                                  @"_label":      @"",
+                                  @"_low-label":  @"",
+                                  @"_high-label": @"",
+                                  @"type":        @"",
+                                  @"arg":         @"",
+                                  @"low":         @"",
+                                  @"high":        @"",
+                                  @"default":     @"",
+                                  @"convert":     @"" }
+                                mutableCopy];
+  [self parseAttrs:dict node:node];
   NSString *label      = [dict objectForKey:@"_label"];
   NSString *low_label  = [dict objectForKey:@"_low-label"];
   NSString *high_label = [dict objectForKey:@"_high-label"];
@@ -588,6 +1344,8 @@ make_number_selector (NSUserDefaultsController *prefs,
   NSString *high       = [dict objectForKey:@"high"];
   NSString *def        = [dict objectForKey:@"default"];
   NSString *cvt        = [dict objectForKey:@"convert"];
+  [dict release];
+  dict = 0;
   
   NSAssert1 (arg,  @"no arg in %@", label);
   NSAssert1 (type, @"no type in %@", label);
@@ -618,18 +1376,20 @@ make_number_selector (NSUserDefaultsController *prefs,
   BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
                   [high rangeOfCharacterFromSet:dot].location != NSNotFound);
 
-  if ([type isEqualToString:@"slider"]) {
+  if ([type isEqualToString:@"slider"]
+# ifdef USE_IPHONE  // On iPhone, we use sliders for all numeric values.
+      || [type isEqualToString:@"spinbutton"]
+# endif
+      ) {
 
     NSRect rect;
     rect.origin.x = rect.origin.y = 0;    
     rect.size.width = 150;
     rect.size.height = 23;  // apparent min height for slider with ticks...
     NSSlider *slider;
-    if (cvt)
-      slider = [[InvertedSlider alloc] initWithFrame:rect];
-    else
-      slider = [[NSSlider alloc] initWithFrame:rect];
-
+    slider = [[InvertedSlider alloc] initWithFrame:rect
+                                     inverted: !!cvt
+                                     integers: !float_p];
     [slider setMaxValue:[high doubleValue]];
     [slider setMinValue:[low  doubleValue]];
     
@@ -639,6 +1399,7 @@ make_number_selector (NSUserDefaultsController *prefs,
     while (range2 > max_ticks)
       range2 /= 10;
 
+# ifndef USE_IPHONE
     // If we have elided ticks, leave it at the max number of ticks.
     if (range != range2 && range2 < max_ticks)
       range2 = max_ticks;
@@ -652,33 +1413,57 @@ make_number_selector (NSUserDefaultsController *prefs,
     [slider setAllowsTickMarkValuesOnly:
               (range == range2 &&  // we are showing the actual number of ticks
                !float_p)];         // and we want integer results
+# endif // !USE_IPHONE
 
     // #### Note: when the slider's range is large enough that we aren't
     //      showing all possible ticks, the slider's value is not constrained
     //      to be an integer, even though it should be...
     //      Maybe we need to use a value converter or something?
 
+    LABEL *lab;
     if (label) {
-      NSTextField *lab = make_label (label);
-      place_child (parent, lab, NO);
-      [lab release];
+      lab = [self makeLabel:label];
+      [self placeChild:lab on:parent];
+# ifdef USE_IPHONE
+      if (low_label) {
+        CGFloat s = [NSFont systemFontSize] + 4;
+        [lab setFont:[NSFont boldSystemFontOfSize:s]];
+      }
+# endif
     }
     
     if (low_label) {
-      NSTextField *lab = make_label (low_label);
+      lab = [self makeLabel:low_label];
       [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
+# ifndef USE_IPHONE
       [lab setAlignment:1];  // right aligned
       rect = [lab frame];
       if (rect.size.width < LEFT_LABEL_WIDTH)
         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
       rect.size.height = [slider frame].size.height;
       [lab setFrame:rect];
-      place_child (parent, lab, NO);
-      [lab release];
+      [self placeChild:lab on:parent];
+# else  // USE_IPHONE
+      [lab setTextAlignment: NSTextAlignmentRight];
+      // Sometimes rotation screws up truncation.
+      [lab setLineBreakMode:NSLineBreakByClipping];
+      [self placeChild:lab on:parent right:(label ? YES : NO)];
+# endif // USE_IPHONE
      }
     
-    place_child (parent, slider, (low_label ? YES : NO));
+# ifndef USE_IPHONE
+    [self placeChild:slider on:parent right:(low_label ? YES : NO)];
+# else  // USE_IPHONE
+    [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
+# endif // USE_IPHONE
     
+    if (low_label) {
+      // Make left label be same height as slider.
+      rect = [lab frame];
+      rect.size.height = [slider frame].size.height;
+      [lab setFrame:rect];
+    }
+
     if (! low_label) {
       rect = [slider frame];
       if (rect.origin.x < LEFT_LABEL_WIDTH)
@@ -687,18 +1472,25 @@ make_number_selector (NSUserDefaultsController *prefs,
     }
         
     if (high_label) {
-      NSTextField *lab = make_label (high_label);
-      [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
+      lab = [self makeLabel:high_label];
+      [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
       rect = [lab frame];
+
+      // Make right label be same height as slider.
       rect.size.height = [slider frame].size.height;
       [lab setFrame:rect];
-      place_child (parent, lab, YES);
-      [lab release];
+# ifdef USE_IPHONE
+      // Sometimes rotation screws up truncation.
+      [lab setLineBreakMode:NSLineBreakByClipping];
+# endif
+      [self placeChild:lab on:parent right:YES];
      }
 
-    bind_switch_to_preferences (prefs, slider, arg, opts);
+    [self bindSwitch:slider cmdline:arg];
     [slider release];
     
+#ifndef USE_IPHONE  // On iPhone, we use sliders for all numeric values.
+
   } else if ([type isEqualToString:@"spinbutton"]) {
 
     if (! label) {
@@ -722,19 +1514,18 @@ make_number_selector (NSUserDefaultsController *prefs,
     [txt setStringValue:@""];
     
     if (label) {
-      NSTextField *lab = make_label (label);
-      //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];      
+      LABEL *lab = [self makeLabel:label];
+      //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
       [lab setAlignment:1];  // right aligned
       rect = [lab frame];
       if (rect.size.width < LEFT_LABEL_WIDTH)
         rect.size.width = LEFT_LABEL_WIDTH;  // make all left labels same size
       rect.size.height = [txt frame].size.height;
       [lab setFrame:rect];
-      place_child (parent, lab, NO);
-      [lab release];
+      [self placeChild:lab on:parent];
      }
     
-    place_child (parent, txt, (label ? YES : NO));
+    [self placeChild:txt on:parent right:(label ? YES : NO)];
     
     if (! label) {
       rect = [txt frame];
@@ -746,7 +1537,7 @@ make_number_selector (NSUserDefaultsController *prefs,
     rect.size.width = rect.size.height = 10;
     NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
     [step sizeToFit];
-    place_child (parent, step, YES);
+    [self placeChild:step on:parent right:YES];
     rect = [step frame];
     rect.origin.x -= COLUMN_SPACING;  // this one goes close
     rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
@@ -776,19 +1567,21 @@ make_number_selector (NSUserDefaultsController *prefs,
     [fmt setGeneratesDecimalNumbers:float_p];
     [[txt cell] setFormatter:fmt];
 
-
-    bind_switch_to_preferences (prefs, step, arg, opts);
-    bind_switch_to_preferences (prefs, txt,  arg, opts);
+    [self bindSwitch:step cmdline:arg];
+    [self bindSwitch:txt  cmdline:arg];
     
     [step release];
     [txt release];
-    
+
+# endif // USE_IPHONE
+
   } else {
     NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
   }
 }
 
 
+# ifndef USE_IPHONE
 static void
 set_menu_item_object (NSMenuItem *item, NSObject *obj)
 {
@@ -811,17 +1604,15 @@ set_menu_item_object (NSMenuItem *item, NSObject *obj)
   [item setRepresentedObject:obj];
   //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
 }
+# endif // !USE_IPHONE
 
 
 /* Creates the popup menu described by the given XML node (and its children).
-*/
-static void
-make_option_menu (NSUserDefaultsController *prefs,
-                  const XrmOptionDescRec *opts, 
-                  NSView *parent, NSXMLNode *node)
+ */
+- (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
 {
   NSArray *children = [node children];
-  int i, count = [children count];
+  NSUInteger i, count = [children count];
 
   if (count <= 0) {
     NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
@@ -830,28 +1621,42 @@ make_option_menu (NSUserDefaultsController *prefs,
 
   // get the "id" attribute off the <select> tag.
   //
-  NSMutableDictionary *dict =
-    [NSMutableDictionary dictionaryWithObjectsAndKeys:
-      @"", @"id",
-      nil];
-  parse_attrs (dict, node);
+  NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
+  [self parseAttrs:dict node:node];
+  [dict release];
+  dict = 0;
   
   NSRect rect;
   rect.origin.x = rect.origin.y = 0;
   rect.size.width = 10;
   rect.size.height = 10;
 
+  NSString *menu_key = nil;   // the resource key used by items in this menu
+
+# ifndef USE_IPHONE
   // #### "Build and Analyze" says that all of our widgets leak, because it
-  //      seems to not realize that place_child -> addSubview retains them.
+  //      seems to not realize that placeChild -> addSubview retains them.
   //      Not sure what to do to make these warnings go away.
 
   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
                                                      pullsDown:NO];
-
   NSMenuItem *def_item = nil;
   float max_width = 0;
-  
-  NSString *menu_key = nil;   // the resource key used by items in this menu
+
+# else  // USE_IPHONE
+
+  NSString *def_item = nil;
+
+  rect.size.width  = 0;
+  rect.size.height = 0;
+#  ifdef USE_PICKER_VIEW
+  UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
+  popup.delegate = self;
+  popup.dataSource = self;
+#  endif // !USE_PICKER_VIEW
+  NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
+
+# endif // USE_IPHONE
   
   for (i = 0; i < count; i++) {
     NSXMLNode *child = [children objectAtIndex:i];
@@ -859,34 +1664,38 @@ make_option_menu (NSUserDefaultsController *prefs,
     if ([child kind] == NSXMLCommentKind)
       continue;
     if ([child kind] != NSXMLElementKind) {
-      NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
+//    NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
       continue;
     }
 
     // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
     //
-    NSMutableDictionary *dict2 =
-      [NSMutableDictionary dictionaryWithObjectsAndKeys:
-        @"", @"id",
-        @"", @"_label",
-        @"", @"arg-set",
-        nil];
-    parse_attrs (dict2, child);
+    NSMutableDictionary *dict2 = [@{ @"id":      @"",
+                                     @"_label":  @"",
+                                     @"arg-set": @"" }
+                                   mutableCopy];
+    [self parseAttrs:dict2 node:child];
     NSString *label   = [dict2 objectForKey:@"_label"];
     NSString *arg_set = [dict2 objectForKey:@"arg-set"];
+    [dict2 release];
+    dict2 = 0;
     
     if (!label) {
       NSAssert1 (0, @"no _label in %@", [child name]);
-      return;
+      continue;
     }
 
+# ifndef USE_IPHONE
     // create the menu item (and then get a pointer to it)
     [popup addItemWithTitle:label];
     NSMenuItem *item = [popup itemWithTitle:label];
+# endif // USE_IPHONE
 
     if (arg_set) {
       NSString *this_val = NULL;
-      NSString *this_key = switch_to_resource (arg_set, opts, &this_val);
+      NSString *this_key = [self switchToResource: arg_set
+                                 opts: opts
+                                 valRet: &this_val];
       NSAssert1 (this_val, @"this_val null for %@", arg_set);
       if (menu_key && ![menu_key isEqualToString:this_key])
         NSAssert3 (0,
@@ -898,26 +1707,41 @@ make_option_menu (NSUserDefaultsController *prefs,
       /* If this menu has the cmd line "-mode foo" then set this item's
          value to "foo" (the menu itself will be bound to e.g. "modeString")
        */
+# ifndef USE_IPHONE
       set_menu_item_object (item, this_val);
+# else
+      // Array holds ["Label", "resource-key", "resource-val"].
+      [items addObject:[NSMutableArray arrayWithObjects:
+                                         label, @"", this_val, nil]];
+# endif
 
     } else {
       // no arg-set -- only one menu item can be missing that.
       NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
+# ifndef USE_IPHONE
       def_item = item;
+# else
+      def_item = label;
+      // Array holds ["Label", "resource-key", "resource-val"].
+      [items addObject:[NSMutableArray arrayWithObjects:
+                                         label, @"", @"", nil]];
+# endif
     }
 
     /* make sure the menu button has room for the text of this item,
        and remember the greatest width it has reached.
      */
+# ifndef USE_IPHONE
     [popup setTitle:label];
     [popup sizeToFit];
     NSRect r = [popup frame];
     if (r.size.width > max_width) max_width = r.size.width;
+# endif // USE_IPHONE
   }
   
   if (!menu_key) {
     NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
-    abort();
+    return;
   }
 
   /* We've added all of the menu items.  If there was an item with no
@@ -927,16 +1751,33 @@ make_option_menu (NSUserDefaultsController *prefs,
      yet know what resource was associated with this menu.)
    */
   if (def_item) {
-    NSDictionary *defs = [prefs initialValues];
-    NSObject *def_obj = [defs objectForKey:menu_key];
-
+    NSObject *def_obj = [defaultOptions objectForKey:menu_key];
     NSAssert2 (def_obj, 
                @"no default value for resource \"%@\" in menu item \"%@\"",
-               menu_key, [def_item title]);
+               menu_key,
+# ifndef USE_IPHONE
+               [def_item title]
+# else
+               def_item
+# endif
+               );
 
+# ifndef USE_IPHONE
     set_menu_item_object (def_item, def_obj);
+# else  // !USE_IPHONE
+    for (NSMutableArray *a in items) {
+      // Make sure each array contains the resource key.
+      [a replaceObjectAtIndex:1 withObject:menu_key];
+      // Make sure the default item contains the default resource value.
+      if (def_obj && def_item &&
+          [def_item isEqualToString:[a objectAtIndex:0]])
+        [a replaceObjectAtIndex:2 withObject:def_obj];
+    }
+# endif // !USE_IPHONE
   }
 
+# ifndef USE_IPHONE
+#  ifdef USE_PICKER_VIEW
   /* Finish tweaking the menu button itself.
    */
   if (def_item)
@@ -944,26 +1785,59 @@ make_option_menu (NSUserDefaultsController *prefs,
   NSRect r = [popup frame];
   r.size.width = max_width;
   [popup setFrame:r];
-  place_child (parent, popup, NO);
+#  endif // USE_PICKER_VIEW
+# endif
 
-  bind_resource_to_preferences (prefs, popup, menu_key, opts);
+# if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
+  [self placeChild:popup on:parent];
+  [self bindResource:popup key:menu_key];
   [popup release];
-}
+# endif
+
+# ifdef USE_IPHONE
+#  ifdef USE_PICKER_VIEW
+  // Store the items for this picker in the picker_values array.
+  // This is so fucking stupid.
+
+  unsigned long menu_number = [pref_keys count] - 1;
+  if (! picker_values)
+    picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
+  while ([picker_values count] <= menu_number)
+    [picker_values addObject:[NSArray arrayWithObjects: nil]];
+  [picker_values replaceObjectAtIndex:menu_number withObject:items];
+  [popup reloadAllComponents];
+
+#  else  // !USE_PICKER_VIEW
+
+  [self placeSeparator];
+
+  i = 0;
+  for (__attribute__((unused)) NSArray *item in items) {
+    RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
+                                          items:items];
+    [b setLineBreakMode:NSLineBreakByTruncatingHead];
+    [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
+    [self placeChild:b on:parent];
+    [b release];
+    i++;
+  }
+
+  [self placeSeparator];
 
+#  endif // !USE_PICKER_VIEW
+# endif // !USE_IPHONE
+
+}
 
-static NSString *unwrap (NSString *);
-static void hreffify (NSText *);
-static void boldify (NSText *);
 
 /* Creates an uneditable, wrapping NSTextField to display the given
    text enclosed by <description> ... </description> in the XML.
  */
-static void
-make_desc_label (NSView *parent, NSXMLNode *node)
+- (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
 {
   NSString *text = nil;
   NSArray *children = [node children];
-  int i, count = [children count];
+  NSUInteger i, count = [children count];
 
   for (i = 0; i < count; i++) {
     NSXMLNode *child = [children objectAtIndex:i];
@@ -980,7 +1854,9 @@ make_desc_label (NSView *parent, NSXMLNode *node)
   rect.origin.x = rect.origin.y = 0;
   rect.size.width = 200;
   rect.size.height = 50;  // sized later
+# ifndef USE_IPHONE
   NSText *lab = [[NSText alloc] initWithFrame:rect];
+  [lab autorelease];
   [lab setEditable:NO];
   [lab setDrawsBackground:NO];
   [lab setHorizontallyResizable:YES];
@@ -990,289 +1866,276 @@ make_desc_label (NSView *parent, NSXMLNode *node)
   boldify (lab);
   [lab sizeToFit];
 
-  place_child (parent, lab, NO);
-  [lab release];
-}
+# else  // USE_IPHONE
 
-static NSString *
-unwrap (NSString *text)
-{
-  // Unwrap lines: delete \n but do not delete \n\n.
-  //
-  NSArray *lines = [text componentsSeparatedByString:@"\n"];
-  int nlines = [lines count];
-  BOOL eolp = YES;
-  int i;
+#  ifndef USE_HTML_LABELS
 
-  text = @"\n";      // start with one blank line
+  UILabel *lab = [self makeLabel:text];
+  [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
+  hreffify (lab);
 
-  // skip trailing blank lines in file
-  for (i = nlines-1; i > 0; i--) {
-    NSString *s = (NSString *) [lines objectAtIndex:i];
-    if ([s length] > 0)
-      break;
-    nlines--;
-  }
+#  else  // USE_HTML_LABELS
+  HTMLLabel *lab = [[HTMLLabel alloc] 
+                     initWithText:text
+                     font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
+  [lab autorelease];
+  [lab setFrame:rect];
+  [lab sizeToFit];
+#  endif // USE_HTML_LABELS
 
-  // skip leading blank lines in file
-  for (i = 0; i < nlines; i++) {
-    NSString *s = (NSString *) [lines objectAtIndex:i];
-    if ([s length] > 0)
-      break;
-  }
+  [self placeSeparator];
 
-  // unwrap
-  Bool any = NO;
-  for (; i < nlines; i++) {
-    NSString *s = (NSString *) [lines objectAtIndex:i];
-    if ([s length] == 0) {
-      text = [text stringByAppendingString:@"\n\n"];
-      eolp = YES;
-    } else if ([s characterAtIndex:0] == ' ' ||
-               [s hasPrefix:@"Copyright "] ||
-               [s hasPrefix:@"http://"]) {
-      // don't unwrap if the following line begins with whitespace,
-      // or with the word "Copyright", or if it begins with a URL.
-      if (any && !eolp)
-        text = [text stringByAppendingString:@"\n"];
-      text = [text stringByAppendingString:s];
-      any = YES;
-      eolp = NO;
-    } else {
-      if (!eolp)
-        text = [text stringByAppendingString:@" "];
-      text = [text stringByAppendingString:s];
-      eolp = NO;
-      any = YES;
-    }
-  }
+# endif // USE_IPHONE
 
-  return text;
+  [self placeChild:lab on:parent];
 }
 
 
-static char *
-anchorize (const char *url)
+/* Creates the NSTextField described by the given XML node.
+ */
+- (void) makeTextField: (NSXMLNode *)node
+                    on: (NSView *)parent
+             withLabel: (BOOL) label_p
+            horizontal: (BOOL) horiz_p
 {
-  const char *wiki = "http://en.wikipedia.org/wiki/";
-  const char *math = "http://mathworld.wolfram.com/";
-  if (!strncmp (wiki, url, strlen(wiki))) {
-    char *anchor = (char *) malloc (strlen(url) * 3 + 10);
-    strcpy (anchor, "Wikipedia: \"");
-    const char *in = url + strlen(wiki);
-    char *out = anchor + strlen(anchor);
-    while (*in) {
-      if (*in == '_') {
-        *out++ = ' ';
-      } else if (*in == '#') {
-        *out++ = ':';
-        *out++ = ' ';
-      } else if (*in == '%') {
-        char hex[3];
-        hex[0] = in[1];
-        hex[1] = in[2];
-        hex[2] = 0;
-        int n = 0;
-        sscanf (hex, "%x", &n);
-        *out++ = (char) n;
-        in += 2;
-      } else {
-        *out++ = *in;
-      }
-      in++;
-    }
-    *out++ = '"';
-    *out = 0;
-    return anchor;
+  NSMutableDictionary *dict = [@{ @"id":     @"",
+                                  @"_label": @"",
+                                  @"arg":    @"" }
+                                mutableCopy];
+  [self parseAttrs:dict node:node];
+  NSString *label = [dict objectForKey:@"_label"];
+  NSString *arg   = [dict objectForKey:@"arg"];
+  [dict release];
+  dict = 0;
 
-  } else if (!strncmp (math, url, strlen(math))) {
-    char *anchor = (char *) malloc (strlen(url) * 3 + 10);
-    strcpy (anchor, "MathWorld: \"");
-    const char *start = url + strlen(wiki);
-    const char *in = start;
-    char *out = anchor + strlen(anchor);
-    while (*in) {
-      if (*in == '_') {
-        *out++ = ' ';
-      } else if (in != start && *in >= 'A' && *in <= 'Z') {
-        *out++ = ' ';
-        *out++ = *in;
-      } else if (!strncmp (in, ".htm", 4)) {
-        break;
-      } else {
-        *out++ = *in;
-      }
-      in++;
-    }
-    *out++ = '"';
-    *out = 0;
-    return anchor;
+  if (!label && label_p) {
+    NSAssert1 (0, @"no _label in %@", [node name]);
+    return;
+  }
 
-  } else {
-    return strdup (url);
+  NSAssert1 (arg, @"no arg in %@", label);
+
+  NSRect rect;
+  rect.origin.x = rect.origin.y = 0;    
+  rect.size.width = rect.size.height = 10;
+  
+  NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
+
+# ifndef USE_IPHONE
+
+  // make the default size be around 30 columns; a typical value for
+  // these text fields is "xscreensaver-text --cols 40".
+  //
+  [txt setStringValue:@"123456789 123456789 123456789 "];
+  [txt sizeToFit];
+  [[txt cell] setWraps:NO];
+  [[txt cell] setScrollable:YES];
+  [txt setStringValue:@""];
+  
+# else  // USE_IPHONE
+
+  txt.adjustsFontSizeToFitWidth = YES;
+  txt.textColor = [UIColor blackColor];
+  txt.font = [UIFont systemFontOfSize: FONT_SIZE];
+  txt.placeholder = @"";
+  txt.borderStyle = UITextBorderStyleRoundedRect;
+  txt.textAlignment = NSTextAlignmentRight;
+  txt.keyboardType = UIKeyboardTypeDefault;  // Full kbd
+  txt.autocorrectionType = UITextAutocorrectionTypeNo;
+  txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
+  txt.clearButtonMode = UITextFieldViewModeAlways;
+  txt.returnKeyType = UIReturnKeyDone;
+  txt.delegate = self;
+  txt.text = @"";
+  [txt setEnabled: YES];
+
+  rect.size.height = [txt.font lineHeight] * 1.2;
+  [txt setFrame:rect];
+
+# endif // USE_IPHONE
+
+  if (label) {
+    LABEL *lab = [self makeLabel:label];
+    [self placeChild:lab on:parent];
   }
+
+  [self placeChild:txt on:parent right:(label ? YES : NO)];
+
+  [self bindSwitch:txt cmdline:arg];
+  [txt release];
 }
 
 
-/* Converts any http: URLs in the given text field to clickable links.
+/* Creates the NSTextField described by the given XML node,
+   and hooks it up to a Choose button and a file selector widget.
  */
-static void
-hreffify (NSText *nstext)
+- (void) makeFileSelector: (NSXMLNode *)node
+                       on: (NSView *)parent
+                 dirsOnly: (BOOL) dirsOnly
+                withLabel: (BOOL) label_p
+                 editable: (BOOL) editable_p
 {
-  NSString *text = [nstext string];
-  [nstext setRichText:YES];
+# ifndef USE_IPHONE    // No files. No selectors.
+  NSMutableDictionary *dict = [@{ @"id":     @"",
+                                  @"_label": @"",
+                                  @"arg":    @"" }
+                                mutableCopy];
+  [self parseAttrs:dict node:node];
+  NSString *label = [dict objectForKey:@"_label"];
+  NSString *arg   = [dict objectForKey:@"arg"];
+  [dict release];
+  dict = 0;
 
-  int L = [text length];
-  NSRange start;               // range is start-of-search to end-of-string
-  start.location = 0;
-  start.length = L;
-  while (start.location < L) {
+  if (!label && label_p) {
+    NSAssert1 (0, @"no _label in %@", [node name]);
+    return;
+  }
 
-    // Find the beginning of a URL...
-    //
-    NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
-    if (r2.location == NSNotFound)
-      break;
+  NSAssert1 (arg, @"no arg in %@", label);
 
-    // Next time around, start searching after this.
-    start.location = r2.location + r2.length;
-    start.length = L - start.location;
+  NSRect rect;
+  rect.origin.x = rect.origin.y = 0;    
+  rect.size.width = rect.size.height = 10;
+  
+  NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
 
-    // Find the end of a URL (whitespace or EOF)...
-    //
-    NSRange r3 = [text rangeOfCharacterFromSet:
-                         [NSCharacterSet whitespaceAndNewlineCharacterSet]
-                       options:0 range:start];
-    if (r3.location == NSNotFound)    // EOF
-      r3.location = L, r3.length = 0;
+  // make the default size be around 20 columns.
+  //
+  [txt setStringValue:@"123456789 123456789 "];
+  [txt sizeToFit];
+  [txt setSelectable:YES];
+  [txt setEditable:editable_p];
+  [txt setBezeled:editable_p];
+  [txt setDrawsBackground:editable_p];
+  [[txt cell] setWraps:NO];
+  [[txt cell] setScrollable:YES];
+  [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
+  [txt setStringValue:@""];
 
-    // Next time around, start searching after this.
-    start.location = r3.location;
-    start.length = L - start.location;
+  LABEL *lab = 0;
+  if (label) {
+    lab = [self makeLabel:label];
+    [self placeChild:lab on:parent];
+  }
 
-    // Set r2 to the start/length of this URL.
-    r2.length = start.location - r2.location;
+  [self placeChild:txt on:parent right:(label ? YES : NO)];
 
-    // Extract the URL.
-    NSString *nsurl = [text substringWithRange:r2];
-    const char *url = [nsurl UTF8String];
+  [self bindSwitch:txt cmdline:arg];
+  [txt release];
 
-    // If this is a Wikipedia URL, make the linked text be prettier.
-    //
-    char *anchor = anchorize(url);
+  // Make the text field and label be the same height, whichever is taller.
+  if (lab) {
+    rect = [txt frame];
+    rect.size.height = ([lab frame].size.height > [txt frame].size.height
+                        ? [lab frame].size.height
+                        : [txt frame].size.height);
+    [txt setFrame:rect];
+  }
 
-    // Construct the RTF corresponding to <A HREF="url">anchor</A>
-    //
-    const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
-    char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
-    sprintf (rtf, fmt, url, anchor);
-    free (anchor);
-    NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
+  // Now put a "Choose" button next to it.
+  //
+  rect.origin.x = rect.origin.y = 0;    
+  rect.size.width = rect.size.height = 10;
+  NSButton *choose = [[NSButton alloc] initWithFrame:rect];
+  [choose setTitle:@"Choose..."];
+  [choose setBezelStyle:NSRoundedBezelStyle];
+  [choose sizeToFit];
 
-    // Insert the RTF into the NSText.
-    [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
+  [self placeChild:choose on:parent right:YES];
 
-    int L2 = [text length];  // might have changed
-    start.location -= (L - L2);
-    L = L2;
-  }
-}
+  // center the Choose button around the midpoint of the text field.
+  rect = [choose frame];
+  rect.origin.y = ([txt frame].origin.y + 
+                   (([txt frame].size.height - rect.size.height) / 2));
+  [choose setFrameOrigin:rect.origin];
 
-/* Makes the text up to the first comma be bold.
- */
-static void
-boldify (NSText *nstext)
-{
-  NSString *text = [nstext string];
-  NSRange r = [text rangeOfString:@"," options:0];
-  r.length = r.location+1;
-  r.location = 0;
+  [choose setTarget:[parent window]];
+  if (dirsOnly)
+    [choose setAction:@selector(fileSelectorChooseDirsAction:)];
+  else
+    [choose setAction:@selector(fileSelectorChooseAction:)];
 
-  NSFont *font = [nstext font];
-  font = [NSFont boldSystemFontOfSize:[font pointSize]];
-  [nstext setFont:font range:r];
+  [choose release];
+# endif // !USE_IPHONE
 }
 
 
-static void layout_group (NSView *group, BOOL horiz_p);
-
+# ifndef USE_IPHONE
 
-/* Creates an invisible NSBox (for layout purposes) to enclose the widgets
-   wrapped in <hgroup> or <vgroup> in the XML.
+/* Runs a modal file selector and sets the text field's value to the
+   selected file or directory.
  */
 static void
-make_group (NSUserDefaultsController *prefs,
-            const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, 
-            BOOL horiz_p)
+do_file_selector (NSTextField *txt, BOOL dirs_p)
 {
-  NSRect rect;
-  rect.size.width = rect.size.height = 1;
-  rect.origin.x = rect.origin.y = 0;
-  NSView *group = [[NSView alloc] initWithFrame:rect];
-  traverse_children (prefs, opts, group, node);
-
-  layout_group (group, horiz_p);
+  NSOpenPanel *panel = [NSOpenPanel openPanel];
+  [panel setAllowsMultipleSelection:NO];
+  [panel setCanChooseFiles:!dirs_p];
+  [panel setCanChooseDirectories:dirs_p];
 
-  rect.size.width = rect.size.height = 0;
-  NSBox *box = [[NSBox alloc] initWithFrame:rect];
-  [box setTitlePosition:NSNoTitle];
-  [box setBorderType:NSNoBorder];
-  [box setContentViewMargins:rect.size];
-  [box setContentView:group];
-  [box sizeToFit];
+  NSInteger result = [panel runModal];
+  if (result == NSOKButton) {
+    NSArray *files = [panel URLs];
+    NSString *file = ([files count] > 0 ? [[files objectAtIndex:0] path] : @"");
+    file = [file stringByAbbreviatingWithTildeInPath];
+    [txt setStringValue:file];
 
-  place_child (parent, box, NO);
+    // Fuck me!  Just setting the value of the NSTextField does not cause
+    // that to end up in the preferences!
+    //
+    NSDictionary *dict = [txt infoForBinding:@"value"];
+    NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
+    NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
+    if ([path hasPrefix:@"values."])  // WTF.
+      path = [path substringFromIndex:7];
+    [[prefs values] setValue:file forKey:path];
+  }
 }
 
 
-static void
-layout_group (NSView *group, BOOL horiz_p)
+/* Returns the NSTextField that is to the left of or above the NSButton.
+ */
+static NSTextField *
+find_text_field_of_button (NSButton *button)
 {
-  NSArray *kids = [group subviews];
-  int nkids = [kids count];
+  NSView *parent = [button superview];
+  NSArray *kids = [parent subviews];
+  NSUInteger nkids = [kids count];
   int i;
-  double maxx = 0, miny = 0;
+  NSTextField *f = 0;
   for (i = 0; i < nkids; i++) {
-    NSView *kid = [kids objectAtIndex:i];
-    NSRect r = [kid frame];
-    
-    if (horiz_p) {
-      maxx += r.size.width + COLUMN_SPACING;
-      if (r.size.height > -miny) miny = -r.size.height;
-    } else {
-      if (r.size.width > maxx)  maxx = r.size.width;
-      miny = r.origin.y - r.size.height;
+    NSObject *kid = [kids objectAtIndex:i];
+    if ([kid isKindOfClass:[NSTextField class]]) {
+      f = (NSTextField *) kid;
+    } else if (kid == button) {
+      if (! f) abort();
+      return f;
     }
   }
-  
-  NSRect rect;
-  rect.origin.x = 0;
-  rect.origin.y = 0;
-  rect.size.width = maxx;
-  rect.size.height = -miny;
-  [group setFrame:rect];
+  abort();
+}
 
-  double x = 0;
-  for (i = 0; i < nkids; i++) {
-    NSView *kid = [kids objectAtIndex:i];
-    NSRect r = [kid frame];
-    if (horiz_p) {
-      r.origin.y = rect.size.height - r.size.height;
-      r.origin.x = x;
-      x += r.size.width + COLUMN_SPACING;
-    } else {
-      r.origin.y -= miny;
-    }
-    [kid setFrame:r];
-  }
+
+- (void) fileSelectorChooseAction:(NSObject *)arg
+{
+  NSButton *choose = (NSButton *) arg;
+  NSTextField *txt = find_text_field_of_button (choose);
+  do_file_selector (txt, NO);
 }
 
+- (void) fileSelectorChooseDirsAction:(NSObject *)arg
+{
+  NSButton *choose = (NSButton *) arg;
+  NSTextField *txt = find_text_field_of_button (choose);
+  do_file_selector (txt, YES);
+}
 
-static void
-make_text_controls (NSUserDefaultsController *prefs,
-                    const XrmOptionDescRec *opts, 
-                    NSView *parent, NSXMLNode *node)
+#endif // !USE_IPHONE
+
+
+- (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
 {
+# ifndef USE_IPHONE
   /*
     Display Text:
      (x)  Computer name and time
@@ -1296,7 +2159,6 @@ make_text_controls (NSUserDefaultsController *prefs,
   Bool program_p = TRUE;
 
 
-  NSXMLElement *node2;
   NSView *control;
 
   // This is how you link radio buttons together.
@@ -1326,19 +2188,75 @@ make_text_controls (NSUserDefaultsController *prefs,
           options:nil];
   [cnames release];
 
-  bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts);
+  [self bindSwitch:matrix cmdline:@"-text-mode %"];
+
+  [self placeChild:matrix on:group];
+  [self placeChild:rgroup on:group right:YES];
+  [proto release];
+  [matrix release];
+  [rgroup release];
+
+  NSXMLNode *node2;
+
+# else  // USE_IPHONE
+
+  NSView *rgroup = parent;
+  NSXMLNode *node2;
+
+  // <select id="textMode">
+  //   <option id="date"  _label="Display date" arg-set="-text-mode date"/>
+  //   <option id="text"  _label="Display text" arg-set="-text-mode literal"/>
+  //   <option id="url"   _label="Display URL"/>
+  // </select>
+
+  node2 = [[NSXMLElement alloc] initWithName:@"select"];
+  [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
+
+  NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
+  [node3 setAttributesAsDictionary:
+           @{ @"id":     @"date",
+              @"arg-set": @"-text-mode date",
+              @"_label":  @"Display the date and time" }];
+  [node3 setParent: node2];
+  [node3 autorelease];
+
+  node3 = [[NSXMLElement alloc] initWithName:@"option"];
+  [node3 setAttributesAsDictionary:
+           @{ @"id":      @"text",
+              @"arg-set": @"-text-mode literal",
+              @"_label":  @"Display static text" }];
+  [node3 setParent: node2];
+  [node3 autorelease];
+
+  node3 = [[NSXMLElement alloc] initWithName:@"option"];
+  [node3 setAttributesAsDictionary:
+           @{ @"id":     @"url",                           
+              @"_label": @"Display the contents of a URL" }];
+  [node3 setParent: node2];
+  [node3 autorelease];
+
+  [self makeOptionMenu:node2 on:rgroup];
+  [node2 release];
+
+# endif // USE_IPHONE
 
-  place_child (group, matrix, NO);
-  place_child (group, rgroup, YES);
 
   //  <string id="textLiteral" _label="" arg-set="-text-literal %"/>
   node2 = [[NSXMLElement alloc] initWithName:@"string"];
   [node2 setAttributesAsDictionary:
-          [NSDictionary dictionaryWithObjectsAndKeys:
-                        @"textLiteral",        @"id",
-                        @"-text-literal %",    @"arg",
-                        nil]];
-  make_text_field (prefs, opts, rgroup, node2, YES);
+           @{ @"id":     @"textLiteral",
+              @"arg":    @"-text-literal %",
+# ifdef USE_IPHONE
+              @"_label": @"Text to display"
+# endif
+            }];
+  [self makeTextField:node2 on:rgroup 
+# ifndef USE_IPHONE
+        withLabel:NO
+# else
+        withLabel:YES
+# endif
+        horizontal:NO];
   [node2 release];
 
 //  rect = [last_child(rgroup) frame];
@@ -1349,42 +2267,51 @@ make_text_controls (NSUserDefaultsController *prefs,
            toObject:[matrix cellAtRow:1 column:0]
            withKeyPath:@"value"
            options:nil];
-*/
+ */
 
 
+# ifndef USE_IPHONE
   //  <file id="textFile" _label="" arg-set="-text-file %"/>
   node2 = [[NSXMLElement alloc] initWithName:@"string"];
   [node2 setAttributesAsDictionary:
-          [NSDictionary dictionaryWithObjectsAndKeys:
-                        @"textFile",           @"id",
-                        @"-text-file %",       @"arg",
-                        nil]];
-  make_file_selector (prefs, opts, rgroup, node2, NO, YES, NO);
+           @{ @"id":  @"textFile",
+              @"arg": @"-text-file %" }];
+  [self makeFileSelector:node2 on:rgroup
+        dirsOnly:NO withLabel:NO editable:NO];
   [node2 release];
+# endif // !USE_IPHONE
 
 //  rect = [last_child(rgroup) frame];
 
   //  <string id="textURL" _label="" arg-set="text-url %"/>
   node2 = [[NSXMLElement alloc] initWithName:@"string"];
   [node2 setAttributesAsDictionary:
-          [NSDictionary dictionaryWithObjectsAndKeys:
-                        @"textURL",            @"id",
-                        @"-text-url %",        @"arg",
-                        nil]];
-  make_text_field (prefs, opts, rgroup, node2, YES);
+           @{ @"id":     @"textURL",            
+              @"arg":    @"-text-url %",
+# ifdef USE_IPHONE
+              @"_label": @"URL to display",     
+# endif
+            }];
+  [self makeTextField:node2 on:rgroup 
+# ifndef USE_IPHONE
+        withLabel:NO
+# else
+        withLabel:YES
+# endif
+        horizontal:NO];
   [node2 release];
 
 //  rect = [last_child(rgroup) frame];
 
+# ifndef USE_IPHONE
   if (program_p) {
     //  <string id="textProgram" _label="" arg-set="text-program %"/>
     node2 = [[NSXMLElement alloc] initWithName:@"string"];
     [node2 setAttributesAsDictionary:
-            [NSDictionary dictionaryWithObjectsAndKeys:
-                          @"textProgram",        @"id",
-                          @"-text-program %",    @"arg",
-                          nil]];
-    make_text_field (prefs, opts, rgroup, node2, YES);
+             @{ @"id":   @"textProgram",
+                 @"arg": @"-text-program %",
+              }];
+    [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
     [node2 release];
   }
 
@@ -1422,103 +2349,425 @@ make_text_controls (NSUserDefaultsController *prefs,
   [matrix setCellSize:rect.size];
   [matrix sizeToCells];
 
-  // Cheat on the position of the stuff on the right (the rgroup).
-  // GAAAH, this code is such crap!
-  rect = [rgroup frame];
-  rect.origin.y -= 5;
-  [rgroup setFrame:rect];
+  // Cheat on the position of the stuff on the right (the rgroup).
+  // GAAAH, this code is such crap!
+  rect = [rgroup frame];
+  rect.origin.y -= 5;
+  [rgroup setFrame:rect];
+
+
+  rect.size.width = rect.size.height = 0;
+  NSBox *box = [[NSBox alloc] initWithFrame:rect];
+  [box setTitlePosition:NSAtTop];
+  [box setBorderType:NSBezelBorder];
+  [box setTitle:@"Display Text"];
+
+  rect.size.width = rect.size.height = 12;
+  [box setContentViewMargins:rect.size];
+  [box setContentView:group];
+  [box sizeToFit];
+
+  [self placeChild:box on:parent];
+  [group release];
+  [box release];
+
+# endif // !USE_IPHONE
+}
+
+
+- (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
+{
+  /*
+    [x]  Grab desktop images
+    [ ]  Choose random image:
+         [__________________________]  [Choose]
+
+   <boolean id="grabDesktopImages" _label="Grab desktop images"
+       arg-unset="-no-grab-desktop"/>
+   <boolean id="chooseRandomImages" _label="Grab desktop images"
+       arg-unset="-choose-random-images"/>
+   <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
+   */
+
+  NSXMLElement *node2;
+
+# ifndef USE_IPHONE
+#  define SCREENS "Grab desktop images"
+#  define PHOTOS  "Choose random images"
+# else
+#  define SCREENS "Grab screenshots"
+#  define PHOTOS  "Use photo library"
+# endif
+
+  node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
+  [node2 setAttributesAsDictionary:
+           @{ @"id":        @"grabDesktopImages",
+              @"_label":    @ SCREENS,
+              @"arg-unset": @"-no-grab-desktop",
+            }];
+  [self makeCheckbox:node2 on:parent];
+  [node2 release];
+
+  node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
+  [node2 setAttributesAsDictionary:
+           @{ @"id":      @"chooseRandomImages",
+              @"_label":  @ PHOTOS,
+              @"arg-set": @"-choose-random-images",
+            }];
+  [self makeCheckbox:node2 on:parent];
+  [node2 release];
+
+  node2 = [[NSXMLElement alloc] initWithName:@"string"];
+  [node2 setAttributesAsDictionary:
+           @{ @"id":     @"imageDirectory",
+              @"_label": @"Images from:",
+              @"arg":    @"-image-directory %",
+            }];
+  [self makeFileSelector:node2 on:parent
+        dirsOnly:YES withLabel:YES editable:YES];
+  [node2 release];
+
+# undef SCREENS
+# undef PHOTOS
+
+# ifndef USE_IPHONE
+  // Add a second, explanatory label below the file/URL selector.
+
+  LABEL *lab2 = 0;
+  lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
+  [self placeChild:lab2 on:parent];
+
+  // Pack it in a little tighter vertically.
+  NSRect r2 = [lab2 frame];
+  r2.origin.x += 20;
+  r2.origin.y += 14;
+  [lab2 setFrameOrigin:r2.origin];
+# endif // USE_IPHONE
+}
+
+
+- (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
+{
+# ifndef USE_IPHONE
+  /*
+    [x]  Check for Updates  [ Monthly ]
+
+  <hgroup>
+   <boolean id="automaticallyChecksForUpdates"
+            _label="Automatically check for updates"
+            arg-unset="-no-automaticallyChecksForUpdates" />
+   <select id="updateCheckInterval">
+    <option="hourly"  _label="Hourly" arg-set="-updateCheckInterval 3600"/>
+    <option="daily"   _label="Daily"  arg-set="-updateCheckInterval 86400"/>
+    <option="weekly"  _label="Weekly" arg-set="-updateCheckInterval 604800"/>
+    <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
+   </select>
+  </hgroup>
+   */
+
+  // <hgroup>
+
+  NSRect rect;
+  rect.size.width = rect.size.height = 1;
+  rect.origin.x = rect.origin.y = 0;
+  NSView *group = [[NSView alloc] initWithFrame:rect];
+
+  NSXMLElement *node2;
+
+  // <boolean ...>
+
+  node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
+  [node2 setAttributesAsDictionary:
+           @{ @"id":        @SUSUEnableAutomaticChecksKey,
+              @"_label":    @"Automatically check for updates",
+              @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
+            }];
+  [self makeCheckbox:node2 on:group];
+  [node2 release];
+
+  // <select ...>
+
+  node2 = [[NSXMLElement alloc] initWithName:@"select"];
+  [node2 setAttributesAsDictionary:
+           @{ @"id": @SUScheduledCheckIntervalKey }];
+
+  //   <option ...>
+
+  NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
+  [node3 setAttributesAsDictionary:
+           @{ @"id":      @"hourly",
+              @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
+              @"_label":  @"Hourly" }];
+  [node3 setParent: node2];
+  [node3 autorelease];
+
+  node3 = [[NSXMLElement alloc] initWithName:@"option"];
+  [node3 setAttributesAsDictionary:
+           @{ @"id":      @"daily",
+              @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
+              @"_label":  @"Daily" }];
+  [node3 setParent: node2];
+  [node3 autorelease];
+
+  node3 = [[NSXMLElement alloc] initWithName:@"option"];
+  [node3 setAttributesAsDictionary:
+           @{ @"id": @"weekly",
+           // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
+              @"_label": @"Weekly",
+            }];
+  [node3 setParent: node2];
+  [node3 autorelease];
+
+  node3 = [[NSXMLElement alloc] initWithName:@"option"];
+  [node3 setAttributesAsDictionary:
+           @{ @"id":      @"monthly",
+              @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
+              @"_label":  @"Monthly",
+             }];
+  [node3 setParent: node2];
+  [node3 autorelease];
+
+  // </option>
+  [self makeOptionMenu:node2 on:group];
+  [node2 release];
+
+  // </hgroup>
+  layout_group (group, TRUE);
+
+  rect.size.width = rect.size.height = 0;
+  NSBox *box = [[NSBox alloc] initWithFrame:rect];
+  [box setTitlePosition:NSNoTitle];
+  [box setBorderType:NSNoBorder];
+  [box setContentViewMargins:rect.size];
+  [box setContentView:group];
+  [box sizeToFit];
+
+  [self placeChild:box on:parent];
+
+  [group release];
+  [box release];
+
+# endif // !USE_IPHONE
+}
+
+
+#pragma mark Layout for controls
+
+
+# ifndef USE_IPHONE
+static NSView *
+last_child (NSView *parent)
+{
+  NSArray *kids = [parent subviews];
+  NSUInteger nkids = [kids count];
+  if (nkids == 0)
+    return 0;
+  else
+    return [kids objectAtIndex:nkids-1];
+}
+#endif // USE_IPHONE
+
+
+/* Add the child as a subview of the parent, positioning it immediately
+   below or to the right of the previously-added child of that view.
+ */
+- (void) placeChild:
+# ifdef USE_IPHONE
+       (NSObject *)child
+# else
+       (NSView *)child
+# endif
+       on:(NSView *)parent right:(BOOL)right_p
+{
+# ifndef USE_IPHONE
+  NSRect rect = [child frame];
+  NSView *last = last_child (parent);
+  if (!last) {
+    rect.origin.x = LEFT_MARGIN;
+    rect.origin.y = ([parent frame].size.height - rect.size.height 
+                     - LINE_SPACING);
+  } else if (right_p) {
+    rect = [last frame];
+    rect.origin.x += rect.size.width + COLUMN_SPACING;
+  } else {
+    rect = [last frame];
+    rect.origin.x = LEFT_MARGIN;
+    rect.origin.y -= [child frame].size.height + LINE_SPACING;
+  }
+  NSRect r = [child frame];
+  r.origin = rect.origin;
+  [child setFrame:r];
+  [parent addSubview:child];
+
+# else // USE_IPHONE
+
+  /* Controls is an array of arrays of the controls, divided into sections.
+     Each hgroup / vgroup gets a nested array, too, e.g.:
+
+       [ [ [ <label>, <checkbox> ],
+           [ <label>, <checkbox> ],
+           [ <label>, <checkbox> ] ],
+         [ <label>, <text-field> ],
+         [ <label>, <low-label>, <slider>, <high-label> ],
+         [ <low-label>, <slider>, <high-label> ],
+         <HTML-label>
+       ];
+
+     If an element begins with a label, it is terminal, otherwise it is a
+     group.  There are (currently) never more than 4 elements in a single
+     terminal element.
+
+     A blank vertical spacer is placed between each hgroup / vgroup,
+     by making each of those a new section in the TableView.
+   */
+  if (! controls)
+    controls = [[NSMutableArray arrayWithCapacity:10] retain];
+  if ([controls count] == 0)
+    [controls addObject: [NSMutableArray arrayWithCapacity:10]];
+  NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
+
+  if (!right_p || [current count] == 0) {
+    // Nothing on the current line. Add this object.
+    [current addObject: child];
+  } else {
+    // Something's on the current line already.
+    NSObject *old = [current objectAtIndex:[current count]-1];
+    if ([old isKindOfClass:[NSMutableArray class]]) {
+      // Already an array in this cell. Append.
+      NSAssert ([(NSArray *) old count] < 4, @"internal error");
+      [(NSMutableArray *) old addObject: child];
+    } else {
+      // Replace the control in this cell with an array, then append
+      NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
+      [current replaceObjectAtIndex:[current count]-1 withObject:a];
+    }
+  }
+# endif // USE_IPHONE
+}
+
+
+- (void) placeChild:(NSView *)child on:(NSView *)parent
+{
+  [self placeChild:child on:parent right:NO];
+}
+
+
+#ifdef USE_IPHONE
+
+// Start putting subsequent children in a new group, to create a new
+// section on the UITableView.
+//
+- (void) placeSeparator
+{
+  if (! controls) return;
+  if ([controls count] == 0) return;
+  if ([[controls objectAtIndex:[controls count]-1]
+        count] > 0)
+    [controls addObject: [NSMutableArray arrayWithCapacity:10]];
+}
+#endif // USE_IPHONE
+
+
+
+/* Creates an invisible NSBox (for layout purposes) to enclose the widgets
+   wrapped in <hgroup> or <vgroup> in the XML.
+ */
+- (void) makeGroup:(NSXMLNode *)node 
+                on:(NSView *)parent
+        horizontal:(BOOL) horiz_p
+{
+# ifdef USE_IPHONE
+  if (!horiz_p) [self placeSeparator];
+  [self traverseChildren:node on:parent];
+  if (!horiz_p) [self placeSeparator];
+# else  // !USE_IPHONE
+  NSRect rect;
+  rect.size.width = rect.size.height = 1;
+  rect.origin.x = rect.origin.y = 0;
+  NSView *group = [[NSView alloc] initWithFrame:rect];
+  [self traverseChildren:node on:group];
 
+  layout_group (group, horiz_p);
 
   rect.size.width = rect.size.height = 0;
   NSBox *box = [[NSBox alloc] initWithFrame:rect];
-  [box setTitlePosition:NSAtTop];
-  [box setBorderType:NSBezelBorder];
-  [box setTitle:@"Display Text"];
-
-  rect.size.width = rect.size.height = 12;
+  [box setTitlePosition:NSNoTitle];
+  [box setBorderType:NSNoBorder];
   [box setContentViewMargins:rect.size];
   [box setContentView:group];
   [box sizeToFit];
 
-  place_child (parent, box, NO);
+  [self placeChild:box on:parent];
+  [group release];
+  [box release];
+# endif // !USE_IPHONE
 }
 
 
+#ifndef USE_IPHONE
 static void
-make_image_controls (NSUserDefaultsController *prefs,
-                     const XrmOptionDescRec *opts, 
-                     NSView *parent, NSXMLNode *node)
+layout_group (NSView *group, BOOL horiz_p)
 {
-  /*
-    [x]  Grab desktop images
-    [ ]  Choose random image:
-         [__________________________]  [Choose]
-
-   <boolean id="grabDesktopImages" _label="Grab desktop images"
-       arg-unset="-no-grab-desktop"/>
-   <boolean id="chooseRandomImages" _label="Grab desktop images"
-       arg-unset="-choose-random-images"/>
-   <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
-   */
-
-  NSXMLElement *node2;
-
-  node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
-  [node2 setAttributesAsDictionary:
-          [NSDictionary dictionaryWithObjectsAndKeys:
-                        @"grabDesktopImages",   @"id",
-                        @"Grab desktop images", @"_label",
-                        @"-no-grab-desktop",    @"arg-unset",
-                        nil]];
-  make_checkbox (prefs, opts, parent, node2);
-  [node2 release];
-
-  node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
-  [node2 setAttributesAsDictionary:
-          [NSDictionary dictionaryWithObjectsAndKeys:
-                        @"chooseRandomImages",    @"id",
-                        @"Choose random images",  @"_label",
-                        @"-choose-random-images", @"arg-set",
-                        nil]];
-  make_checkbox (prefs, opts, parent, node2);
-  [node2 release];
-
-  node2 = [[NSXMLElement alloc] initWithName:@"string"];
-  [node2 setAttributesAsDictionary:
-          [NSDictionary dictionaryWithObjectsAndKeys:
-                        @"imageDirectory",     @"id",
-                        @"Images from:",       @"_label",
-                        @"-image-directory %", @"arg",
-                        nil]];
-  make_file_selector (prefs, opts, parent, node2, YES, NO, YES);
-  [node2 release];
-
-  // Add a second, explanatory label below the file/URL selector.
-
-  NSTextField *lab2 = 0;
-  lab2 = make_label (@"(Local folder, or URL of RSS or Atom feed)");
-  place_child (parent, lab2, NO);
+  NSArray *kids = [group subviews];
+  NSUInteger nkids = [kids count];
+  NSUInteger i;
+  double maxx = 0, miny = 0;
+  for (i = 0; i < nkids; i++) {
+    NSView *kid = [kids objectAtIndex:i];
+    NSRect r = [kid frame];
+    
+    if (horiz_p) {
+      maxx += r.size.width + COLUMN_SPACING;
+      if (r.size.height > -miny) miny = -r.size.height;
+    } else {
+      if (r.size.width > maxx)  maxx = r.size.width;
+      miny = r.origin.y - r.size.height;
+    }
+  }
+  
+  NSRect rect;
+  rect.origin.x = 0;
+  rect.origin.y = 0;
+  rect.size.width = maxx;
+  rect.size.height = -miny;
+  [group setFrame:rect];
 
-  // Pack it in a little tighter vertically.
-  NSRect r2 = [lab2 frame];
-  r2.origin.x += 20;
-  r2.origin.y += 14;
-  [lab2 setFrameOrigin:r2.origin];
-  [lab2 release];
+  double x = 0;
+  for (i = 0; i < nkids; i++) {
+    NSView *kid = [kids objectAtIndex:i];
+    NSRect r = [kid frame];
+    if (horiz_p) {
+      r.origin.y = rect.size.height - r.size.height;
+      r.origin.x = x;
+      x += r.size.width + COLUMN_SPACING;
+    } else {
+      r.origin.y -= miny;
+    }
+    [kid setFrame:r];
+  }
 }
-
+#endif // !USE_IPHONE
 
 
 /* Create some kind of control corresponding to the given XML node.
  */
-static void
-make_control (NSUserDefaultsController *prefs,
-              const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node)
+-(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
 {
   NSString *name = [node name];
 
   if ([node kind] == NSXMLCommentKind)
     return;
+
+  if ([node kind] == NSXMLTextKind) {
+    NSString *s = [(NSString *) [node objectValue]
+                   stringByTrimmingCharactersInSet:
+                    [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+    if (! [s isEqualToString:@""]) {
+      NSAssert1 (0, @"unexpected text: %@", s);
+    }
+    return;
+  }
+
   if ([node kind] != NSXMLElementKind) {
     NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
     return;
@@ -1527,35 +2776,42 @@ make_control (NSUserDefaultsController *prefs,
   if ([name isEqualToString:@"hgroup"] ||
       [name isEqualToString:@"vgroup"]) {
 
-    BOOL horiz_p = [name isEqualToString:@"hgroup"];
-    make_group (prefs, opts, parent, node, horiz_p);
+    [self makeGroup:node on:parent 
+          horizontal:[name isEqualToString:@"hgroup"]];
 
   } else if ([name isEqualToString:@"command"]) {
     // do nothing: this is the "-root" business
 
+  } else if ([name isEqualToString:@"video"]) {
+    // ignored
+
   } else if ([name isEqualToString:@"boolean"]) {
-    make_checkbox (prefs, opts, parent, node);
+    [self makeCheckbox:node on:parent];
 
   } else if ([name isEqualToString:@"string"]) {
-    make_text_field (prefs, opts, parent, node, NO);
+    [self makeTextField:node on:parent withLabel:NO horizontal:NO];
 
   } else if ([name isEqualToString:@"file"]) {
-    make_file_selector (prefs, opts, parent, node, NO, NO, NO);
+    [self makeFileSelector:node on:parent
+          dirsOnly:NO withLabel:YES editable:NO];
 
   } else if ([name isEqualToString:@"number"]) {
-    make_number_selector (prefs, opts, parent, node);
+    [self makeNumberSelector:node on:parent];
 
   } else if ([name isEqualToString:@"select"]) {
-    make_option_menu (prefs, opts, parent, node);
+    [self makeOptionMenu:node on:parent];
 
   } else if ([name isEqualToString:@"_description"]) {
-    make_desc_label (parent, node);
+    [self makeDescLabel:node on:parent];
 
   } else if ([name isEqualToString:@"xscreensaver-text"]) {
-    make_text_controls (prefs, opts, parent, node);
+    [self makeTextLoaderControlBox:node on:parent];
 
   } else if ([name isEqualToString:@"xscreensaver-image"]) {
-    make_image_controls (prefs, opts, parent, node);
+    [self makeImageLoaderControlBox:node on:parent];
+
+  } else if ([name isEqualToString:@"xscreensaver-updater"]) {
+    [self makeUpdaterControlBox:node on:parent];
 
   } else {
     NSAssert1 (0, @"unknown tag: %@", name);
@@ -1565,45 +2821,18 @@ make_control (NSUserDefaultsController *prefs,
 
 /* Iterate over and process the children of this XML node.
  */
-static void
-traverse_children (NSUserDefaultsController *prefs,
-                   const XrmOptionDescRec *opts,
-                   NSView *parent, NSXMLNode *node)
+- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
 {
   NSArray *children = [node children];
-  int i, count = [children count];
+  NSUInteger i, count = [children count];
   for (i = 0; i < count; i++) {
     NSXMLNode *child = [children objectAtIndex:i];
-    make_control (prefs, opts, parent, child);
+    [self makeControl:child on:parent];
   }
 }
 
-/* Handle the options on the top level <xscreensaver> tag.
- */
-static void
-parse_xscreensaver_tag (NSXMLNode *node)
-{
-  NSMutableDictionary *dict =
-  [NSMutableDictionary dictionaryWithObjectsAndKeys:
-    @"", @"name",
-    @"", @"_label",
-    nil];
-  parse_attrs (dict, node);
-  NSString *name  = [dict objectForKey:@"name"];
-  NSString *label = [dict objectForKey:@"_label"];
-    
-  if (!label) {
-    NSAssert1 (0, @"no _label in %@", [node name]);
-    return;
-  }
-  if (!name) {
-    NSAssert1 (0, @"no name in \"%@\"", label);
-    return;
-  }
-  
-  // #### do any callers need the "name" field for anything?
-}
 
+# ifndef USE_IPHONE
 
 /* Kludgey magic to make the window enclose the controls we created.
  */
@@ -1612,10 +2841,10 @@ fix_contentview_size (NSView *parent)
 {
   NSRect f;
   NSArray *kids = [parent subviews];
-  int nkids = [kids count];
+  NSUInteger nkids = [kids count];
   NSView *text = 0;  // the NSText at the bottom of the window
   double maxx = 0, miny = 0;
-  int i;
+  NSUInteger i;
 
   /* Find the size of the rectangle taken up by each of the children
      except the final "NSText" child.
@@ -1715,14 +2944,14 @@ fix_contentview_size (NSView *parent)
   }
   
 /*
-Bad:
- parent: 420 x 541 @   0   0
- text:   380 x 100 @  20  22  miny=-501
+    Bad:
    parent: 420 x 541 @   0   0
    text:   380 x 100 @  20  22  miny=-501
 
-Good:
- parent: 420 x 541 @   0   0
- text:   380 x 100 @  20  50  miny=-501
-*/
+    Good:
    parent: 420 x 541 @   0   0
    text:   380 x 100 @  20  50  miny=-501
+ */
 
   // #### WTF2: See "WTF" above.  If the text field is off the screen,
   //      move it up.  We need this on 10.6 but not on 10.5.  Auugh.
@@ -1744,29 +2973,11 @@ Good:
     [kid setAutoresizingMask:mask];
   }
 }
+# endif // !USE_IPHONE
 
 
-- (void) okClicked:(NSObject *)arg
-{
-  [userDefaultsController commitEditing];
-  [userDefaultsController save:self];
-  [NSApp endSheet:self returnCode:NSOKButton];
-  [self close];
-}
-
-- (void) cancelClicked:(NSObject *)arg
-{
-  [userDefaultsController revert:self];
-  [NSApp endSheet:self returnCode:NSCancelButton];
-  [self close];
-}
-
-- (void) resetClicked:(NSObject *)arg
-{
-  [userDefaultsController revertToInitialValues:self];
-}
-
 
+#ifndef USE_IPHONE
 static NSView *
 wrap_with_buttons (NSWindow *window, NSView *panel)
 {
@@ -1870,85 +3081,611 @@ wrap_with_buttons (NSWindow *window, NSView *panel)
   [ok     setTarget:window];
   [cancel setTarget:window];
   [reset  setTarget:window];
-  [ok     setAction:@selector(okClicked:)];
-  [cancel setAction:@selector(cancelClicked:)];
-  [reset  setAction:@selector(resetClicked:)];
+  [ok     setAction:@selector(okAction:)];
+  [cancel setAction:@selector(cancelAction:)];
+  [reset  setAction:@selector(resetAction:)];
   
+  [bbox release];
+
   return pbox;
 }
+#endif // !USE_IPHONE
 
 
 /* Iterate over and process the children of the root node of the XML document.
  */
-static void
-traverse_tree (NSUserDefaultsController *prefs,
-               NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node)
+- (void)traverseTree
 {
+# ifdef USE_IPHONE
+  NSView *parent = [self view];
+# else
+  NSWindow *parent = self;
+#endif
+  NSXMLNode *node = xml_root;
+
   if (![[node name] isEqualToString:@"screensaver"]) {
     NSAssert (0, @"top level node is not <xscreensaver>");
   }
 
-  parse_xscreensaver_tag (node);
+  saver_name = [self parseXScreenSaverTag: node];
+  saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
+                           withString:@""];
+  [saver_name retain];
   
+# ifndef USE_IPHONE
+
   NSRect rect;
   rect.origin.x = rect.origin.y = 0;
   rect.size.width = rect.size.height = 1;
 
   NSView *panel = [[NSView alloc] initWithFrame:rect];
-  
-  traverse_children (prefs, opts, panel, node);
+  [self traverseChildren:node on:panel];
   fix_contentview_size (panel);
 
-  NSView *root = wrap_with_buttons (window, panel);
-  [prefs setAppliesImmediately:NO];
+  NSView *root = wrap_with_buttons (parent, panel);
+  [userDefaultsController   setAppliesImmediately:NO];
+  [globalDefaultsController setAppliesImmediately:NO];
 
   [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
 
-  rect = [window frameRectForContentRect:[root frame]];
-  [window setFrame:rect display:NO];
-  [window setMinSize:rect.size];
+  rect = [parent frameRectForContentRect:[root frame]];
+  [parent setFrame:rect display:NO];
+  [parent setMinSize:rect.size];
   
-  [window setContentView:root];
+  [parent setContentView:root];
+       
+  [panel release];
+  [root release];
+
+# else  // USE_IPHONE
+
+  CGRect r = [parent frame];
+  r.size = [[UIScreen mainScreen] bounds].size;
+  [parent setFrame:r];
+  [self traverseChildren:node on:parent];
+
+# endif // USE_IPHONE
+}
+
+
+- (void)parser:(NSXMLParser *)parser
+        didStartElement:(NSString *)elt
+        namespaceURI:(NSString *)ns
+        qualifiedName:(NSString *)qn
+        attributes:(NSDictionary *)attrs
+{
+  NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
+  [e autorelease];
+  [e setKind:SimpleXMLElementKind];
+  [e setAttributesAsDictionary:attrs];
+  NSXMLElement *p = xml_parsing;
+  [e setParent:p];
+  xml_parsing = e;
+  if (! xml_root)
+    xml_root = xml_parsing;
+}
+
+- (void)parser:(NSXMLParser *)parser
+        didEndElement:(NSString *)elt
+        namespaceURI:(NSString *)ns
+        qualifiedName:(NSString *)qn
+{
+  NSXMLElement *p = xml_parsing;
+  if (! p) {
+    NSLog(@"extra close: %@", elt);
+  } else if (![[p name] isEqualToString:elt]) {
+    NSLog(@"%@ closed by %@", [p name], elt);
+  } else {
+    NSXMLElement *n = xml_parsing;
+    xml_parsing = [n parent];
+  }
+}
+
+
+- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
+{
+  NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
+  [e setKind:SimpleXMLTextKind];
+  NSXMLElement *p = xml_parsing;
+  [e setParent:p];
+  [e setObjectValue: string];
+  [e autorelease];
+}
+
+
+# ifdef USE_IPHONE
+# ifdef USE_PICKER_VIEW
+
+#pragma mark UIPickerView delegate methods
+
+- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
+{
+  return 1;    // Columns
+}
+
+- (NSInteger)pickerView:(UIPickerView *)pv
+             numberOfRowsInComponent:(NSInteger)column
+{
+  NSAssert (column == 0, @"weird column");
+  NSArray *a = [picker_values objectAtIndex: [pv tag]];
+  if (! a) return 0;  // Too early?
+  return [a count];
+}
+
+- (CGFloat)pickerView:(UIPickerView *)pv
+           rowHeightForComponent:(NSInteger)column
+{
+  return FONT_SIZE;
+}
+
+- (CGFloat)pickerView:(UIPickerView *)pv
+           widthForComponent:(NSInteger)column
+{
+  NSAssert (column == 0, @"weird column");
+  NSArray *a = [picker_values objectAtIndex: [pv tag]];
+  if (! a) return 0;  // Too early?
+
+  UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
+  CGFloat max = 0;
+  for (NSArray *a2 in a) {
+    NSString *s = [a2 objectAtIndex:0];
+    // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize.
+    CGSize r = [s sizeWithFont:f];
+    if (r.width > max) max = r.width;
+  }
+
+  max *= 1.7;  // WTF!!
+
+  if (max > 320)
+    max = 320;
+  else if (max < 120)
+    max = 120;
+
+  return max;
+
+}
+
+
+- (NSString *)pickerView:(UIPickerView *)pv
+              titleForRow:(NSInteger)row
+              forComponent:(NSInteger)column
+{
+  NSAssert (column == 0, @"weird column");
+  NSArray *a = [picker_values objectAtIndex: [pv tag]];
+  if (! a) return 0;  // Too early?
+  a = [a objectAtIndex:row];
+  NSAssert (a, @"internal error");
+  return [a objectAtIndex:0];
+}
+
+# endif // USE_PICKER_VIEW
+
+
+#pragma mark UITableView delegate methods
+
+- (void) addResetButton
+{
+  [[self navigationItem] 
+    setRightBarButtonItem: [[UIBarButtonItem alloc]
+                             initWithTitle: @"Reset to Defaults"
+                             style: UIBarButtonItemStylePlain
+                             target:self
+                             action:@selector(resetAction:)]];
+  NSString *s = saver_name;
+  if ([self view].frame.size.width > 320)
+    s = [s stringByAppendingString: @" Settings"];
+  [self navigationItem].title = s;
+}
+
+
+- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
+{
+  return YES;
+}
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
+  // Number of vertically-stacked white boxes.
+  return [controls count];
+}
+
+- (NSInteger)tableView:(UITableView *)tableView
+             numberOfRowsInSection:(NSInteger)section
+{
+  // Number of lines in each vertically-stacked white box.
+  NSAssert (controls, @"internal error");
+  return [[controls objectAtIndex:section] count];
+}
+
+- (NSString *)tableView:(UITableView *)tv
+              titleForHeaderInSection:(NSInteger)section
+{
+  // Titles above each vertically-stacked white box.
+//  if (section == 0)
+//    return [saver_name stringByAppendingString:@" Settings"];
+  return nil;
 }
 
 
+- (CGFloat)tableView:(UITableView *)tv
+           heightForRowAtIndexPath:(NSIndexPath *)ip
+{
+  CGFloat h = 0;
+
+  NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
+                  objectAtIndex:[ip indexAtPosition:1]];
+
+  if ([ctl isKindOfClass:[NSArray class]]) {
+    NSArray *set = (NSArray *) ctl;
+    switch ([set count]) {
+    case 4:                    // label + left/slider/right.
+    case 3:                    // left/slider/right.
+      h = FONT_SIZE * 3.0;
+      break;
+    case 2:                    // Checkboxes, or text fields.
+      h = FONT_SIZE * 2.4;
+      break;
+    }
+  } else if ([ctl isKindOfClass:[UILabel class]]) {
+    // Radio buttons in a multi-select list.
+    h = FONT_SIZE * 1.9;
+
+# ifdef USE_HTML_LABELS
+  } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
+    
+    HTMLLabel *t = (HTMLLabel *) ctl;
+    CGRect r = t.frame;
+    r.size.width = [tv frame].size.width;
+    r.size.width -= LEFT_MARGIN * 2;
+    [t setFrame:r];
+    [t sizeToFit];
+    r = t.frame;
+    h = r.size.height;
+# endif // USE_HTML_LABELS
+
+  } else {                     // Does this ever happen?
+    h = FONT_SIZE + LINE_SPACING * 2;
+  }
+
+  if (h <= 0) abort();
+  return h;
+}
+
+
+- (void)refreshTableView
+{
+  UITableView *tv = (UITableView *) [self view];
+  NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
+  NSInteger rows = [self numberOfSectionsInTableView:tv];
+  for (int i = 0; i < rows; i++) {
+    NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
+    for (int j = 0; j < cols; j++) {
+      NSUInteger ip[2];
+      ip[0] = i;
+      ip[1] = j;
+      [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
+    }
+  }
+
+  [tv beginUpdates];
+  [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
+  [tv endUpdates];
+
+  // Default opacity looks bad.
+  // #### Oh great, this only works *sometimes*.
+  UIView *v = [[self navigationItem] titleView];
+  [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
+}
+
+
+- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
+{
+  [NSTimer scheduledTimerWithTimeInterval: 0
+           target:self
+           selector:@selector(refreshTableView)
+           userInfo:nil
+           repeats:NO];
+}
+
+
+#ifndef USE_PICKER_VIEW
+
+- (void)updateRadioGroupCell:(UITableViewCell *)cell
+                      button:(RadioButton *)b
+{
+  NSArray *item = [[b items] objectAtIndex: [b index]];
+  NSString *pref_key = [item objectAtIndex:1];
+  NSObject *pref_val = [item objectAtIndex:2];
+
+  NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
+
+  // Convert them both to strings and compare those, so that
+  // we don't get screwed by int 1 versus string "1".
+  // Will boolean true/1 screw us here too?
+  //
+  NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
+                        ? (NSString *) pref_val
+                        : [(NSNumber *) pref_val stringValue]);
+  NSString *current_str = ([current isKindOfClass:[NSString class]]
+                           ? (NSString *) current
+                           : [(NSNumber *) current stringValue]);
+  BOOL match_p = [current_str isEqualToString:pref_str];
+
+  // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
+
+  if (match_p)
+    [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
+  else
+    [cell setAccessoryType:UITableViewCellAccessoryNone];
+}
+
+
+- (void)tableView:(UITableView *)tv
+        didSelectRowAtIndexPath:(NSIndexPath *)ip
+{
+  RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
+                       objectAtIndex:[ip indexAtPosition:1]];
+  if (! [ctl isKindOfClass:[RadioButton class]])
+    return;
+
+  [self radioAction:ctl];
+  [self refreshTableView];
+}
+
+
+#endif // !USE_PICKER_VIEW
+
+
+
+- (UITableViewCell *)tableView:(UITableView *)tv
+                     cellForRowAtIndexPath:(NSIndexPath *)ip
+{
+  CGFloat ww = [tv frame].size.width;
+  CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
+
+  float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
+
+  // Width of the column of labels on the left.
+  CGFloat left_width = ww * 0.4;
+  CGFloat right_edge = ww - LEFT_MARGIN;
+
+  if (os_version < 7)  // margins were wider on iOS 6.1
+    right_edge -= 10;
+
+  CGFloat max = FONT_SIZE * 12;
+  if (left_width > max) left_width = max;
+
+  NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
+                           objectAtIndex:[ip indexAtPosition:1]];
+
+  if ([ctl isKindOfClass:[NSArray class]]) {
+    // This cell has a set of objects in it.
+    NSArray *set = (NSArray *) ctl;
+    switch ([set count]) {
+    case 2:
+      {
+        // With 2 elements, the first of the pair must be a label.
+        UILabel *label = (UILabel *) [set objectAtIndex: 0];
+        NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
+        ctl = [set objectAtIndex: 1];
+
+        CGRect r = [ctl frame];
+
+        if ([ctl isKindOfClass:[UISwitch class]]) {    // Checkboxes.
+          r.size.width = 80;  // Magic.
+          r.origin.x = right_edge - r.size.width + 30;  // beats me
+
+          if (os_version < 7)  // checkboxes were wider on iOS 6.1
+            r.origin.x -= 25;
+
+        } else {
+          r.origin.x = left_width;                     // Text fields, etc.
+          r.size.width = right_edge - r.origin.x;
+        }
+
+        r.origin.y = (hh - r.size.height) / 2;   // Center vertically.
+        [ctl setFrame:r];
+
+        // Make a box and put the label and checkbox/slider into it.
+        r.origin.x = 0;
+        r.origin.y = 0;
+        r.size.width  = ww;
+        r.size.height = hh;
+        NSView *box = [[UIView alloc] initWithFrame:r];
+        [box addSubview: ctl];
+
+        // Let the label make use of any space not taken up by the control.
+        r = [label frame];
+        r.origin.x = LEFT_MARGIN;
+        r.origin.y = 0;
+        r.size.width  = [ctl frame].origin.x - r.origin.x;
+        r.size.height = hh;
+        [label setFrame:r];
+        [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
+        [box addSubview: label];
+        [box autorelease];
+
+        ctl = box;
+      }
+      break;
+    case 3:
+    case 4:
+      {
+        // With 3 elements, 1 and 3 are labels.
+        // With 4 elements, 1, 2 and 4 are labels.
+        int i = 0;
+        UILabel *top  = ([set count] == 4
+                         ? [set objectAtIndex: i++]
+                         : 0);
+        UILabel *left  = [set objectAtIndex: i++];
+        NSView  *mid   = [set objectAtIndex: i++];
+        UILabel *right = [set objectAtIndex: i++];
+        NSAssert (!top || [top   isKindOfClass:[UILabel class]], @"WTF");
+        NSAssert (        [left  isKindOfClass:[UILabel class]], @"WTF");
+        NSAssert (       ![mid   isKindOfClass:[UILabel class]], @"WTF");
+        NSAssert (        [right isKindOfClass:[UILabel class]], @"WTF");
+
+        // 3 elements: control at top of cell.
+        // 4 elements: center the control vertically.
+        CGRect r = [mid frame];
+        r.size.height = 32;   // Unchangable height of the slider thumb.
+
+        // Center the slider between left_width and right_edge.
+# ifdef  LABEL_ABOVE_SLIDER
+        r.origin.x = LEFT_MARGIN;
+# else
+        r.origin.x = left_width;
+# endif
+        r.origin.y = (hh - r.size.height) / 2;
+        r.size.width = right_edge - r.origin.x;
+        [mid setFrame:r];
+
+        if (top) {
+# ifdef LABEL_ABOVE_SLIDER
+          // Top label goes above, flush center/top.
+          r.origin.x = (ww - r.size.width) / 2;
+          r.origin.y = 4;
+          // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize.
+          r.size = [[top text] sizeWithFont:[top font]
+                               constrainedToSize:
+                                 CGSizeMake (ww - LEFT_MARGIN*2, 100000)
+                               lineBreakMode:[top lineBreakMode]];
+# else  // !LABEL_ABOVE_SLIDER
+          // Label goes on the left.
+          r.origin.x = LEFT_MARGIN;
+          r.origin.y = 0;
+          r.size.width  = left_width - LEFT_MARGIN;
+          r.size.height = hh;
+# endif // !LABEL_ABOVE_SLIDER
+          [top setFrame:r];
+        }
+
+        // Left label goes under control, flush left/bottom.
+        left.frame = CGRectMake([mid frame].origin.x, hh - 4,
+                                ww - LEFT_MARGIN*2, 100000);
+        [left sizeToFit];
+        r = left.frame;
+        r.origin.y -= r.size.height;
+        left.frame = r;
+
+        // Right label goes under control, flush right/bottom.
+        right.frame =
+          CGRectMake([mid frame].origin.x + [mid frame].size.width,
+                     [left frame].origin.y, ww - LEFT_MARGIN*2, 1000000);
+        [right sizeToFit];
+        r = right.frame;
+        r.origin.x -= r.size.width;
+        right.frame = r;
+
+        // Make a box and put the labels and slider into it.
+        r.origin.x = 0;
+        r.origin.y = 0;
+        r.size.width  = ww;
+        r.size.height = hh;
+        NSView *box = [[UIView alloc] initWithFrame:r];
+        if (top)
+          [box addSubview: top];
+        [box addSubview: left];
+        [box addSubview: right];
+        [box addSubview: mid];
+        [box autorelease];
+
+        ctl = box;
+      }
+      break;
+    default:
+      NSAssert (0, @"unhandled size");
+    }
+  } else {     // A single view, not a pair.
+    CGRect r = [ctl frame];
+    r.origin.x = LEFT_MARGIN;
+    r.origin.y = 0;
+    r.size.width = right_edge - r.origin.x;
+    r.size.height = hh;
+    [ctl setFrame:r];
+  }
+
+  NSString *id = @"Cell";
+  UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
+  if (!cell)
+    cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
+                                     reuseIdentifier: id]
+             autorelease];
+
+  for (UIView *subview in [cell.contentView subviews])
+    [subview removeFromSuperview];
+  [cell.contentView addSubview: ctl];
+  CGRect r = [ctl frame];
+  r.origin.x = 0;
+  r.origin.y = 0;
+  [cell setFrame:r];
+  cell.selectionStyle = UITableViewCellSelectionStyleNone;
+  [cell setAccessoryType:UITableViewCellAccessoryNone];
+
+# ifndef USE_PICKER_VIEW
+  if ([ctl isKindOfClass:[RadioButton class]])
+    [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
+# endif // USE_PICKER_VIEW
+
+  return cell;
+}
+# endif  // USE_IPHONE
+
+
 /* When this object is instantiated, it parses the XML file and creates
    controls on itself that are hooked up to the appropriate preferences.
    The default size of the view is just big enough to hold them all.
  */
-- (id)initWithXMLFile: (NSString *) xml_file
-              options: (const XrmOptionDescRec *) opts
-           controller: (NSUserDefaultsController *) prefs
+- (id)initWithXML: (NSData *) xml_data
+          options: (const XrmOptionDescRec *) _opts
+       controller: (NSUserDefaultsController *) _prefs
+ globalController: (NSUserDefaultsController *) _globalPrefs
+         defaults: (NSDictionary *) _defs
 {
-  if (! (self = [super init]))
-    return 0;
-
-  // instance variable
-  userDefaultsController = prefs;
-  [prefs retain];
-
-  NSURL *furl = [NSURL fileURLWithPath:xml_file];
-
-  if (!furl) {
-    NSAssert1 (0, @"can't URLify \"%@\"", xml_file);
+# ifndef USE_IPHONE
+  self = [super init];
+# else  // !USE_IPHONE
+  self = [super initWithStyle:UITableViewStyleGrouped];
+  self.title = [saver_name stringByAppendingString:@" Settings"];
+# endif // !USE_IPHONE
+  if (! self) return 0;
+
+  // instance variables
+  opts = _opts;
+  defaultOptions = _defs;
+  userDefaultsController   = [_prefs retain];
+  globalDefaultsController = [_globalPrefs retain];
+
+  NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
+
+  if (!xmlDoc) {
+    NSAssert1 (0, @"XML Error: %@",
+               [[NSString alloc] initWithData:xml_data
+                                 encoding:NSUTF8StringEncoding]);
     return nil;
   }
-
-  NSError *err = nil;
-  NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] 
-                            initWithContentsOfURL:furl
-                            options:(NSXMLNodePreserveWhitespace |
-                                     NSXMLNodePreserveCDATA)
-                            error:&err];
-  if (!xmlDoc || err) {
-    if (err)
-      NSAssert2 (0, @"XML Error: %@: %@",
-                 xml_file, [err localizedDescription]);
+  [xmlDoc setDelegate:self];
+  if (! [xmlDoc parse]) {
+    NSError *err = [xmlDoc parserError];
+    NSAssert2 (0, @"XML Error: %@: %@",
+               [[NSString alloc] initWithData:xml_data
+                                 encoding:NSUTF8StringEncoding],
+               err);
     return nil;
   }
 
-  traverse_tree (prefs, self, opts, [xmlDoc rootElement]);
-  [xmlDoc release];
+# ifndef USE_IPHONE
+  TextModeTransformer *t = [[TextModeTransformer alloc] init];
+  [NSValueTransformer setValueTransformer:t
+                      forName:@"TextModeTransformer"];
+  [t release];
+# endif // USE_IPHONE
+
+  [self traverseTree];
+  xml_root = 0;
+
+# ifdef USE_IPHONE
+  [self addResetButton];
+# endif
 
   return self;
 }
@@ -1956,7 +3693,17 @@ traverse_tree (NSUserDefaultsController *prefs,
 
 - (void) dealloc
 {
+  [saver_name release];
   [userDefaultsController release];
+  [globalDefaultsController release];
+# ifdef USE_IPHONE
+  [controls release];
+  [pref_keys release];
+  [pref_ctls release];
+#  ifdef USE_PICKER_VIEW
+  [picker_values release];
+#  endif
+# endif
   [super dealloc];
 }