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