1 /* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski <jwz@jwz.org>
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
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."
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).
26 #import "XScreenSaverConfigSheet.h"
30 #import "InvertedSlider.h"
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
48 # define LABEL NSTextField
51 #undef LABEL_ABOVE_SLIDER
52 #define USE_HTML_LABELS
55 #pragma mark XML Parser
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.
64 #define NSXMLNode SimpleXMLNode
65 #define NSXMLElement SimpleXMLNode
66 #define NSXMLCommentKind SimpleXMLCommentKind
67 #define NSXMLElementKind SimpleXMLElementKind
68 #define NSXMLAttributeKind SimpleXMLAttributeKind
69 #define NSXMLTextKind SimpleXMLTextKind
71 typedef enum { SimpleXMLCommentKind,
73 SimpleXMLAttributeKind,
77 @interface SimpleXMLNode : NSObject
81 SimpleXMLNode *parent;
82 NSMutableArray *children;
83 NSMutableArray *attributes;
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:)
97 @implementation SimpleXMLNode
101 //@synthesize parent;
102 @synthesize children;
103 @synthesize attributes;
109 attributes = [NSMutableArray arrayWithCapacity:10];
114 - (id) initWithName:(NSString *)n
117 [self setKind:NSXMLElementKind];
123 - (void) setAttributesAsDictionary:(NSDictionary *)dict
125 for (NSString *key in dict) {
126 NSObject *val = [dict objectForKey:key];
127 SimpleXMLNode *n = [[SimpleXMLNode alloc] init];
128 [n setKind:SimpleXMLAttributeKind];
130 [n setObjectValue:val];
131 [attributes addObject:n];
135 - (SimpleXMLNode *) parent { return parent; }
137 - (void) setParent:(SimpleXMLNode *)p
139 NSAssert (!parent, @"parent already set");
142 NSMutableArray *kids = [p children];
144 kids = [NSMutableArray arrayWithCapacity:10];
145 [p setChildren:kids];
147 [kids addObject:self];
152 #pragma mark Implementing radio buttons
154 /* The UIPickerView is a hideous and uncustomizable piece of shit.
155 I can't believe Apple actually released that thing on the world.
156 Let's fake up some radio buttons instead.
159 #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
161 @interface RadioButton : UILabel
167 @property(nonatomic) int index;
168 @property(nonatomic, retain) NSArray *items;
172 @implementation RadioButton
177 - (id) initWithIndex:(int)_index items:_items
179 self = [super initWithFrame:CGRectZero];
181 items = [_items retain];
183 [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
184 [self setBackgroundColor:[UIColor clearColor]];
193 # endif // !USE_PICKER_VIEW
196 # pragma mark Implementing labels with clickable links
198 #if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
200 @interface HTMLLabel : UIView <UIWebViewDelegate>
207 @property(nonatomic, retain) NSString *html;
208 @property(nonatomic, retain) UIWebView *webView;
210 - (id) initWithHTML:(NSString *)h font:(UIFont *)f;
211 - (id) initWithText:(NSString *)t font:(UIFont *)f;
212 - (void) setHTML:(NSString *)h;
213 - (void) setText:(NSString *)t;
218 @implementation HTMLLabel
223 - (id) initWithHTML:(NSString *)h font:(UIFont *)f
226 if (! self) return 0;
228 webView = [[UIWebView alloc] init];
229 webView.delegate = self;
230 webView.dataDetectorTypes = UIDataDetectorTypeNone;
231 self. autoresizingMask = (UIViewAutoresizingFlexibleWidth |
232 UIViewAutoresizingFlexibleHeight);
233 webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
234 UIViewAutoresizingFlexibleHeight);
235 [self addSubview: webView];
240 - (id) initWithText:(NSString *)t font:(UIFont *)f
242 self = [self initWithHTML:@"" font:f];
243 if (! self) return 0;
249 - (void) setHTML: (NSString *)h
253 if (html) [html release];
256 [NSString stringWithFormat:
257 @"<!DOCTYPE HTML PUBLIC "
258 "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
259 " \"http://www.w3.org/TR/html4/loose.dtd\">"
262 // "<META NAME=\"viewport\" CONTENT=\""
263 // "width=device-width"
264 // "initial-scale=1.0;"
265 // "maximum-scale=1.0;\">"
269 " margin: 0; padding: 0; border: 0;"
270 " font-family: \"%@\";"
271 " font-size: %.4fpx;" // Must be "px", not "pt"!
272 " line-height: %.4fpx;" // And no spaces before it.
285 [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
289 static char *anchorize (const char *url);
291 - (void) setText: (NSString *)t
293 t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
294 t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
295 t = [t stringByReplacingOccurrencesOfString:@">" withString:@">"];
296 t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
297 t = [t stringByReplacingOccurrencesOfString:@"<P> "
298 withString:@"<P> "];
299 t = [t stringByReplacingOccurrencesOfString:@"\n "
300 withString:@"<BR> "];
304 [t componentsSeparatedByCharactersInSet:
305 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
306 if ([s hasPrefix:@"http://"] ||
307 [s hasPrefix:@"https://"]) {
308 char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
309 NSString *a2 = [NSString stringWithCString: anchor
310 encoding: NSUTF8StringEncoding];
311 s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
314 h = [NSString stringWithFormat: @"%@ %@", h, s];
320 -(BOOL) webView:(UIWebView *)wv
321 shouldStartLoadWithRequest:(NSURLRequest *)req
322 navigationType:(UIWebViewNavigationType)type
324 // Force clicked links to open in Safari, not in this window.
325 if (type == UIWebViewNavigationTypeLinkClicked) {
326 [[UIApplication sharedApplication] openURL:[req URL]];
333 - (void) setFrame: (CGRect)r
338 [webView setFrame: r];
339 [self setHTML: html];
344 - (NSString *) stripTags:(NSString *)str
346 NSString *result = @"";
348 str = [str stringByReplacingOccurrencesOfString:@"<P>"
349 withString:@"<BR><BR>"
350 options:NSCaseInsensitiveSearch
351 range:NSMakeRange(0, [str length])];
352 str = [str stringByReplacingOccurrencesOfString:@"<BR>"
354 options:NSCaseInsensitiveSearch
355 range:NSMakeRange(0, [str length])];
357 for (NSString *s in [str componentsSeparatedByString: @"<"]) {
358 NSRange r = [s rangeOfString:@">"];
360 s = [s substringFromIndex: r.location + r.length];
361 result = [result stringByAppendingString: s];
369 CGRect r = [self frame];
371 /* It would be sensible to just ask the UIWebView how tall the page is,
372 instead of hoping that NSString and UIWebView measure fonts and do
373 wrapping in exactly the same way, but I can't make that work.
374 Maybe because it loads async?
377 r.size.height = [[webView
378 stringByEvaluatingJavaScriptFromString:
379 @"document.body.offsetHeight"]
382 NSString *text = [self stripTags: html];
385 s = [text sizeWithFont: font
387 lineBreakMode:NSLineBreakByWordWrapping];
389 // GAAAH. Add one more line, or the UIWebView is still scrollable!
390 // The text is sized right, but it lets you scroll it up anyway.
391 s.height += [font pointSize];
393 r.size.height = s.height;
410 #endif // USE_IPHONE && USE_HTML_LABELS
413 @interface XScreenSaverConfigSheet (Private)
415 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
418 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
419 - (void) placeChild: (NSView *)c on:(NSView *)p;
420 static NSView *last_child (NSView *parent);
421 static void layout_group (NSView *group, BOOL horiz_p);
423 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
424 - (void) placeChild: (NSObject *)c on:(NSView *)p;
425 - (void) placeSeparator;
426 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
427 - (void) refreshTableView;
428 # endif // USE_IPHONE
433 @implementation XScreenSaverConfigSheet
435 # define LEFT_MARGIN 20 // left edge of window
436 # define COLUMN_SPACING 10 // gap between e.g. labels and text fields
437 # define LEFT_LABEL_WIDTH 70 // width of all left labels
438 # define LINE_SPACING 10 // leading between each line
440 # define FONT_SIZE 17 // Magic hardcoded UITableView font size.
442 #pragma mark Talking to the resource database
445 /* Normally we read resources by looking up "KEY" in the database
446 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
447 app, everything is stored in the database "org.jwz.xscreensaver"
448 instead, so transform keys to "SAVERNAME.KEY".
450 NOTE: This is duplicated in PrefsReader.m, cause I suck.
452 - (NSString *) makeKey:(NSString *)key
455 NSString *prefix = [saver_name stringByAppendingString:@"."];
456 if (! [key hasPrefix:prefix]) // Don't double up!
457 key = [prefix stringByAppendingString:key];
463 - (NSString *) makeCKey:(const char *)key
465 return [self makeKey:[NSString stringWithCString:key
466 encoding:NSUTF8StringEncoding]];
470 /* Given a command-line option, returns the corresponding resource name.
471 Any arguments in the switch string are ignored (e.g., "-foo x").
473 - (NSString *) switchToResource:(NSString *)cmdline_switch
474 opts:(const XrmOptionDescRec *)opts_array
475 valRet:(NSString **)val_ret
479 NSAssert(cmdline_switch, @"cmdline switch is null");
480 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
481 encoding:NSUTF8StringEncoding]) {
482 NSAssert1(0, @"unable to convert %@", cmdline_switch);
485 char *s = strpbrk(buf, " \t\r\n");
489 while (*tail && (*tail == ' ' || *tail == '\t'))
493 while (opts_array[0].option) {
494 if (!strcmp (opts_array[0].option, buf)) {
497 if (opts_array[0].argKind == XrmoptionNoArg) {
499 NSAssert1 (0, @"expected no args to switch: \"%@\"",
501 ret = opts_array[0].value;
504 NSAssert1 (0, @"expected args to switch: \"%@\"",
511 ? [NSString stringWithCString:ret
512 encoding:NSUTF8StringEncoding]
515 const char *res = opts_array[0].specifier;
516 while (*res && (*res == '.' || *res == '*'))
518 return [self makeCKey:res];
523 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
528 - (NSUserDefaultsController *)controllerForKey:(NSString *)key
530 static NSDictionary *a = 0;
532 a = UPDATER_DEFAULTS;
535 if ([a objectForKey:key])
536 // These preferences are global to all xscreensavers.
537 return globalDefaultsController;
539 // All other preferences are per-saver.
540 return userDefaultsController;
546 // Called when a slider is bonked.
548 - (void)sliderAction:(UISlider*)sender
550 if ([active_text_field canResignFirstResponder])
551 [active_text_field resignFirstResponder];
552 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
554 // Hacky API. See comment in InvertedSlider.m.
555 double v = ([sender isKindOfClass: [InvertedSlider class]]
556 ? [(InvertedSlider *) sender transformedValue]
559 [[self controllerForKey:pref_key]
560 setObject:((v == (int) v)
561 ? [NSNumber numberWithInt:(int) v]
562 : [NSNumber numberWithDouble: v])
566 // Called when a checkbox/switch is bonked.
568 - (void)switchAction:(UISwitch*)sender
570 if ([active_text_field canResignFirstResponder])
571 [active_text_field resignFirstResponder];
572 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
573 NSString *v = ([sender isOn] ? @"true" : @"false");
574 [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
577 # ifdef USE_PICKER_VIEW
578 // Called when a picker is bonked.
580 - (void)pickerView:(UIPickerView *)pv
581 didSelectRow:(NSInteger)row
582 inComponent:(NSInteger)column
584 if ([active_text_field canResignFirstResponder])
585 [active_text_field resignFirstResponder];
587 NSAssert (column == 0, @"internal error");
588 NSArray *a = [picker_values objectAtIndex: [pv tag]];
589 if (! a) return; // Too early?
590 a = [a objectAtIndex:row];
591 NSAssert (a, @"missing row");
593 //NSString *label = [a objectAtIndex:0];
594 NSString *pref_key = [a objectAtIndex:1];
595 NSObject *pref_val = [a objectAtIndex:2];
596 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
598 # else // !USE_PICKER_VIEW
600 // Called when a RadioButton is bonked.
602 - (void)radioAction:(RadioButton*)sender
604 if ([active_text_field canResignFirstResponder])
605 [active_text_field resignFirstResponder];
607 NSArray *item = [[sender items] objectAtIndex: [sender index]];
608 NSString *pref_key = [item objectAtIndex:1];
609 NSObject *pref_val = [item objectAtIndex:2];
610 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
613 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
615 active_text_field = tf;
619 - (void)textFieldDidEndEditing:(UITextField *)tf
621 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
622 NSString *txt = [tf text];
623 [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
626 - (BOOL)textFieldShouldReturn:(UITextField *)tf
628 active_text_field = nil;
629 [tf resignFirstResponder];
633 # endif // !USE_PICKER_VIEW
640 - (void) okAction:(NSObject *)arg
642 [userDefaultsController commitEditing];
643 [globalDefaultsController commitEditing];
644 [userDefaultsController save:self];
645 [globalDefaultsController save:self];
646 [NSApp endSheet:self returnCode:NSOKButton];
650 - (void) cancelAction:(NSObject *)arg
652 [userDefaultsController revert:self];
653 [globalDefaultsController revert:self];
654 [NSApp endSheet:self returnCode:NSCancelButton];
657 # endif // !USE_IPHONE
660 - (void) resetAction:(NSObject *)arg
663 [userDefaultsController revertToInitialValues:self];
664 [globalDefaultsController revertToInitialValues:self];
667 for (NSString *key in defaultOptions) {
668 NSObject *val = [defaultOptions objectForKey:key];
669 [[self controllerForKey:key] setObject:val forKey:key];
672 for (UIControl *ctl in pref_ctls) {
673 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
674 [self bindResource:ctl key:pref_key reload:YES];
677 [self refreshTableView];
678 # endif // USE_IPHONE
682 /* Connects a control (checkbox, etc) to the corresponding preferences key.
684 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
685 reload:(BOOL)reload_p
687 NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
689 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
691 : ([control isKindOfClass:[NSMatrix class]]
696 withKeyPath:[@"values." stringByAppendingString: pref_key]
700 NSObject *val = [prefs objectForKey:pref_key];
704 if ([val isKindOfClass:[NSString class]]) {
705 sval = (NSString *) val;
706 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
707 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
708 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
711 dval = [sval doubleValue];
712 } else if ([val isKindOfClass:[NSNumber class]]) {
713 // NSBoolean (__NSCFBoolean) is really NSNumber.
714 dval = [(NSNumber *) val doubleValue];
715 sval = [(NSNumber *) val stringValue];
718 if ([control isKindOfClass:[UISlider class]]) {
719 sel = @selector(sliderAction:);
720 // Hacky API. See comment in InvertedSlider.m.
721 if ([control isKindOfClass:[InvertedSlider class]])
722 [(InvertedSlider *) control setTransformedValue: dval];
724 [(UISlider *) control setValue: dval];
725 } else if ([control isKindOfClass:[UISwitch class]]) {
726 sel = @selector(switchAction:);
727 [(UISwitch *) control setOn: ((int) dval != 0)];
728 # ifdef USE_PICKER_VIEW
729 } else if ([control isKindOfClass:[UIPickerView class]]) {
731 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
733 # else // !USE_PICKER_VIEW
734 } else if ([control isKindOfClass:[RadioButton class]]) {
735 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
736 } else if ([control isKindOfClass:[UITextField class]]) {
738 [(UITextField *) control setText: sval];
739 # endif // !USE_PICKER_VIEW
741 NSAssert (0, @"unknown class");
744 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
748 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
749 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
752 [pref_keys addObject: [self makeKey:pref_key]];
753 [pref_ctls addObject: control];
754 ((UIControl *) control).tag = [pref_keys count] - 1;
757 [(UIControl *) control addTarget:self action:sel
758 forControlEvents:UIControlEventValueChanged];
762 # endif // USE_IPHONE
765 NSObject *def = [[prefs defaults] objectForKey:pref_key];
766 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
767 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
768 s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
769 s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
770 NSLog (@"%@ %@/%@", s, [def class], [control class]);
775 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
777 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
782 - (void) bindSwitch:(NSObject *)control
783 cmdline:(NSString *)cmd
785 [self bindResource:control
786 key:[self switchToResource:cmd opts:opts valRet:0]];
790 #pragma mark Text-manipulating utilities
794 unwrap (NSString *text)
796 // Unwrap lines: delete \n but do not delete \n\n.
798 NSArray *lines = [text componentsSeparatedByString:@"\n"];
799 NSUInteger i, nlines = [lines count];
802 text = @"\n"; // start with one blank line
804 // skip trailing blank lines in file
805 for (i = nlines-1; i > 0; i--) {
806 NSString *s = (NSString *) [lines objectAtIndex:i];
812 // skip leading blank lines in file
813 for (i = 0; i < nlines; i++) {
814 NSString *s = (NSString *) [lines objectAtIndex:i];
821 for (; i < nlines; i++) {
822 NSString *s = (NSString *) [lines objectAtIndex:i];
823 if ([s length] == 0) {
824 text = [text stringByAppendingString:@"\n\n"];
826 } else if ([s characterAtIndex:0] == ' ' ||
827 [s hasPrefix:@"Copyright "] ||
828 [s hasPrefix:@"http://"]) {
829 // don't unwrap if the following line begins with whitespace,
830 // or with the word "Copyright", or if it begins with a URL.
832 text = [text stringByAppendingString:@"\n"];
833 text = [text stringByAppendingString:s];
838 text = [text stringByAppendingString:@" "];
839 text = [text stringByAppendingString:s];
850 /* Makes the text up to the first comma be bold.
853 boldify (NSText *nstext)
855 NSString *text = [nstext string];
856 NSRange r = [text rangeOfString:@"," options:0];
857 r.length = r.location+1;
861 NSFont *font = [nstext font];
862 font = [NSFont boldSystemFontOfSize:[font pointSize]];
863 [nstext setFont:font range:r];
865 # endif // !USE_IPHONE
868 /* Creates a human-readable anchor to put on a URL.
871 anchorize (const char *url)
873 const char *wiki = "http://en.wikipedia.org/wiki/";
874 const char *math = "http://mathworld.wolfram.com/";
875 if (!strncmp (wiki, url, strlen(wiki))) {
876 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
877 strcpy (anchor, "Wikipedia: \"");
878 const char *in = url + strlen(wiki);
879 char *out = anchor + strlen(anchor);
883 } else if (*in == '#') {
886 } else if (*in == '%') {
892 sscanf (hex, "%x", &n);
904 } else if (!strncmp (math, url, strlen(math))) {
905 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
906 strcpy (anchor, "MathWorld: \"");
907 const char *start = url + strlen(wiki);
908 const char *in = start;
909 char *out = anchor + strlen(anchor);
913 } else if (in != start && *in >= 'A' && *in <= 'Z') {
916 } else if (!strncmp (in, ".htm", 4)) {
933 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
935 /* Converts any http: URLs in the given text field to clickable links.
938 hreffify (NSText *nstext)
941 NSString *text = [nstext string];
942 [nstext setRichText:YES];
944 NSString *text = [nstext text];
947 int L = [text length];
948 NSRange start; // range is start-of-search to end-of-string
951 while (start.location < L) {
953 // Find the beginning of a URL...
955 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
956 if (r2.location == NSNotFound)
959 // Next time around, start searching after this.
960 start.location = r2.location + r2.length;
961 start.length = L - start.location;
963 // Find the end of a URL (whitespace or EOF)...
965 NSRange r3 = [text rangeOfCharacterFromSet:
966 [NSCharacterSet whitespaceAndNewlineCharacterSet]
967 options:0 range:start];
968 if (r3.location == NSNotFound) // EOF
969 r3.location = L, r3.length = 0;
971 // Next time around, start searching after this.
972 start.location = r3.location;
973 start.length = L - start.location;
975 // Set r2 to the start/length of this URL.
976 r2.length = start.location - r2.location;
979 NSString *nsurl = [text substringWithRange:r2];
980 const char *url = [nsurl UTF8String];
982 // If this is a Wikipedia URL, make the linked text be prettier.
984 char *anchor = anchorize(url);
988 // Construct the RTF corresponding to <A HREF="url">anchor</A>
990 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
991 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
992 sprintf (rtf, fmt, url, anchor);
994 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
995 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
997 # else // !USE_IPHONE
998 // *anchor = 0; // Omit Wikipedia anchor
999 text = [text stringByReplacingCharactersInRange:r2
1000 withString:[NSString stringWithCString:anchor
1001 encoding:NSUTF8StringEncoding]];
1002 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1003 // withString:@"\n\n"];
1004 # endif // !USE_IPHONE
1008 int L2 = [text length]; // might have changed
1009 start.location -= (L - L2);
1014 [nstext setText:text];
1019 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1023 #pragma mark Creating controls from XML
1026 /* Parse the attributes of an XML tag into a dictionary.
1027 For input, the dictionary should have as attributes the keys, each
1028 with @"" as their value.
1029 On output, the dictionary will set the keys to the values specified,
1030 and keys that were not specified will not be present in the dictionary.
1031 Warnings are printed if there are duplicate or unknown attributes.
1033 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1035 NSArray *attrs = [(NSXMLElement *) node attributes];
1036 NSUInteger n = [attrs count];
1039 // For each key in the dictionary, fill in the dict with the corresponding
1040 // value. The value @"" is assumed to mean "un-set". Issue a warning if
1041 // an attribute is specified twice.
1043 for (i = 0; i < n; i++) {
1044 NSXMLNode *attr = [attrs objectAtIndex:i];
1045 NSString *key = [attr name];
1046 NSString *val = [attr objectValue];
1047 NSString *old = [dict objectForKey:key];
1050 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1051 } else if ([old length] != 0) {
1052 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1054 [dict setValue:val forKey:key];
1058 // Remove from the dictionary any keys whose value is still @"",
1059 // meaning there was no such attribute specified.
1061 NSArray *keys = [dict allKeys];
1063 for (i = 0; i < n; i++) {
1064 NSString *key = [keys objectAtIndex:i];
1065 NSString *val = [dict objectForKey:key];
1066 if ([val length] == 0)
1067 [dict removeObjectForKey:key];
1071 // Kludge for starwars.xml:
1072 // If there is a "_low-label" and no "_label", but "_low-label" contains
1073 // spaces, divide them.
1074 NSString *lab = [dict objectForKey:@"_label"];
1075 NSString *low = [dict objectForKey:@"_low-label"];
1078 [[[low stringByTrimmingCharactersInSet:
1079 [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1080 componentsSeparatedByString: @" "]
1081 filteredArrayUsingPredicate:
1082 [NSPredicate predicateWithFormat:@"length > 0"]];
1083 if (split && [split count] == 2) {
1084 [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1085 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1088 # endif // USE_IPHONE
1092 /* Handle the options on the top level <xscreensaver> tag.
1094 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1096 NSMutableDictionary *dict = [@{ @"name": @"",
1100 [self parseAttrs:dict node:node];
1101 NSString *name = [dict objectForKey:@"name"];
1102 NSString *label = [dict objectForKey:@"_label"];
1104 NSAssert1 (label, @"no _label in %@", [node name]);
1105 NSAssert1 (name, @"no name in \"%@\"", label);
1110 /* Creates a label: an un-editable NSTextField displaying the given text.
1112 - (LABEL *) makeLabel:(NSString *)text
1115 rect.origin.x = rect.origin.y = 0;
1116 rect.size.width = rect.size.height = 10;
1118 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1119 [lab setSelectable:NO];
1120 [lab setEditable:NO];
1121 [lab setBezeled:NO];
1122 [lab setDrawsBackground:NO];
1123 [lab setStringValue:text];
1125 # else // USE_IPHONE
1126 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1127 [lab setText: [text stringByTrimmingCharactersInSet:
1128 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1129 [lab setBackgroundColor:[UIColor clearColor]];
1130 [lab setNumberOfLines:0]; // unlimited
1131 // [lab setLineBreakMode:UILineBreakModeWordWrap];
1132 [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1133 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1134 UIViewAutoresizingFlexibleHeight)];
1135 # endif // USE_IPHONE
1140 /* Creates the checkbox (NSButton) described by the given XML node.
1142 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1144 NSMutableDictionary *dict = [@{ @"id": @"",
1149 [self parseAttrs:dict node:node];
1150 NSString *label = [dict objectForKey:@"_label"];
1151 NSString *arg_set = [dict objectForKey:@"arg-set"];
1152 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1155 NSAssert1 (0, @"no _label in %@", [node name]);
1158 if (!arg_set && !arg_unset) {
1159 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
1162 if (arg_set && arg_unset) {
1163 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
1167 // sanity-check the choice of argument names.
1169 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1170 [arg_set hasPrefix:@"--no-"]))
1171 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1173 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1174 ![arg_unset hasPrefix:@"--no-"]))
1175 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1179 rect.origin.x = rect.origin.y = 0;
1180 rect.size.width = rect.size.height = 10;
1184 NSButton *button = [[NSButton alloc] initWithFrame:rect];
1185 [button setButtonType:NSSwitchButton];
1186 [button setTitle:label];
1188 [self placeChild:button on:parent];
1190 # else // USE_IPHONE
1192 LABEL *lab = [self makeLabel:label];
1193 [self placeChild:lab on:parent];
1194 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1195 [self placeChild:button on:parent right:YES];
1198 # endif // USE_IPHONE
1200 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1205 /* Creates the number selection control described by the given XML node.
1206 If "type=slider", it's an NSSlider.
1207 If "type=spinbutton", it's a text field with up/down arrows next to it.
1209 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1211 NSMutableDictionary *dict = [@{ @"id": @"",
1214 @"_high-label": @"",
1222 [self parseAttrs:dict node:node];
1223 NSString *label = [dict objectForKey:@"_label"];
1224 NSString *low_label = [dict objectForKey:@"_low-label"];
1225 NSString *high_label = [dict objectForKey:@"_high-label"];
1226 NSString *type = [dict objectForKey:@"type"];
1227 NSString *arg = [dict objectForKey:@"arg"];
1228 NSString *low = [dict objectForKey:@"low"];
1229 NSString *high = [dict objectForKey:@"high"];
1230 NSString *def = [dict objectForKey:@"default"];
1231 NSString *cvt = [dict objectForKey:@"convert"];
1233 NSAssert1 (arg, @"no arg in %@", label);
1234 NSAssert1 (type, @"no type in %@", label);
1237 NSAssert1 (0, @"no low in %@", [node name]);
1241 NSAssert1 (0, @"no high in %@", [node name]);
1245 NSAssert1 (0, @"no default in %@", [node name]);
1248 if (cvt && ![cvt isEqualToString:@"invert"]) {
1249 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1253 // If either the min or max field contains a decimal point, then this
1254 // option may have a floating point value; otherwise, it is constrained
1255 // to be an integer.
1257 NSCharacterSet *dot =
1258 [NSCharacterSet characterSetWithCharactersInString:@"."];
1259 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1260 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1262 if ([type isEqualToString:@"slider"]
1263 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1264 || [type isEqualToString:@"spinbutton"]
1269 rect.origin.x = rect.origin.y = 0;
1270 rect.size.width = 150;
1271 rect.size.height = 23; // apparent min height for slider with ticks...
1273 slider = [[InvertedSlider alloc] initWithFrame:rect
1275 integers: !float_p];
1276 [slider setMaxValue:[high doubleValue]];
1277 [slider setMinValue:[low doubleValue]];
1279 int range = [slider maxValue] - [slider minValue] + 1;
1282 while (range2 > max_ticks)
1285 // If we have elided ticks, leave it at the max number of ticks.
1286 if (range != range2 && range2 < max_ticks)
1289 // If it's a float, always display the max number of ticks.
1290 if (float_p && range2 < max_ticks)
1294 [slider setNumberOfTickMarks:range2];
1296 [slider setAllowsTickMarkValuesOnly:
1297 (range == range2 && // we are showing the actual number of ticks
1298 !float_p)]; // and we want integer results
1299 # endif // !USE_IPHONE
1301 // #### Note: when the slider's range is large enough that we aren't
1302 // showing all possible ticks, the slider's value is not constrained
1303 // to be an integer, even though it should be...
1304 // Maybe we need to use a value converter or something?
1308 lab = [self makeLabel:label];
1309 [self placeChild:lab on:parent];
1312 CGFloat s = [NSFont systemFontSize] + 4;
1313 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1320 lab = [self makeLabel:low_label];
1321 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1323 [lab setAlignment:1]; // right aligned
1325 if (rect.size.width < LEFT_LABEL_WIDTH)
1326 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1327 rect.size.height = [slider frame].size.height;
1328 [lab setFrame:rect];
1329 [self placeChild:lab on:parent];
1330 # else // USE_IPHONE
1331 [lab setTextAlignment: NSTextAlignmentRight];
1332 [self placeChild:lab on:parent right:(label ? YES : NO)];
1333 # endif // USE_IPHONE
1339 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1340 # else // USE_IPHONE
1341 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1342 # endif // USE_IPHONE
1345 // Make left label be same height as slider.
1347 rect.size.height = [slider frame].size.height;
1348 [lab setFrame:rect];
1352 rect = [slider frame];
1353 if (rect.origin.x < LEFT_LABEL_WIDTH)
1354 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1355 [slider setFrame:rect];
1359 lab = [self makeLabel:high_label];
1360 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1363 // Make right label be same height as slider.
1364 rect.size.height = [slider frame].size.height;
1365 [lab setFrame:rect];
1366 [self placeChild:lab on:parent right:YES];
1370 [self bindSwitch:slider cmdline:arg];
1373 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1375 } else if ([type isEqualToString:@"spinbutton"]) {
1378 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1381 NSAssert1 (!low_label,
1382 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1383 NSAssert1 (!high_label,
1384 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1385 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1389 rect.origin.x = rect.origin.y = 0;
1390 rect.size.width = rect.size.height = 10;
1392 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1393 [txt setStringValue:@"0000.0"];
1395 [txt setStringValue:@""];
1398 LABEL *lab = [self makeLabel:label];
1399 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1400 [lab setAlignment:1]; // right aligned
1402 if (rect.size.width < LEFT_LABEL_WIDTH)
1403 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1404 rect.size.height = [txt frame].size.height;
1405 [lab setFrame:rect];
1406 [self placeChild:lab on:parent];
1410 [self placeChild:txt on:parent right:(label ? YES : NO)];
1414 if (rect.origin.x < LEFT_LABEL_WIDTH)
1415 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1416 [txt setFrame:rect];
1419 rect.size.width = rect.size.height = 10;
1420 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1422 [self placeChild:step on:parent right:YES];
1423 rect = [step frame];
1424 rect.origin.x -= COLUMN_SPACING; // this one goes close
1425 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1426 [step setFrame:rect];
1428 [step setMinValue:[low doubleValue]];
1429 [step setMaxValue:[high doubleValue]];
1430 [step setAutorepeat:YES];
1431 [step setValueWraps:NO];
1433 double range = [high doubleValue] - [low doubleValue];
1435 [step setIncrement:range / 10.0];
1436 else if (range >= 500)
1437 [step setIncrement:range / 100.0];
1439 [step setIncrement:1.0];
1441 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1442 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1443 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1444 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1445 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1446 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1447 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1449 [fmt setGeneratesDecimalNumbers:float_p];
1450 [[txt cell] setFormatter:fmt];
1452 [self bindSwitch:step cmdline:arg];
1453 [self bindSwitch:txt cmdline:arg];
1458 # endif // USE_IPHONE
1461 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1468 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1470 /* If the object associated with this menu item looks like a boolean,
1471 store an NSNumber instead of an NSString, since that's what
1472 will be in the preferences (due to similar logic in PrefsReader).
1474 if ([obj isKindOfClass:[NSString class]]) {
1475 NSString *string = (NSString *) obj;
1476 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1477 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1478 obj = [NSNumber numberWithBool:YES];
1479 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1480 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1481 obj = [NSNumber numberWithBool:NO];
1486 [item setRepresentedObject:obj];
1487 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1489 # endif // !USE_IPHONE
1492 /* Creates the popup menu described by the given XML node (and its children).
1494 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1496 NSArray *children = [node children];
1497 NSUInteger i, count = [children count];
1500 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1504 // get the "id" attribute off the <select> tag.
1506 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1507 [self parseAttrs:dict node:node];
1510 rect.origin.x = rect.origin.y = 0;
1511 rect.size.width = 10;
1512 rect.size.height = 10;
1514 NSString *menu_key = nil; // the resource key used by items in this menu
1517 // #### "Build and Analyze" says that all of our widgets leak, because it
1518 // seems to not realize that placeChild -> addSubview retains them.
1519 // Not sure what to do to make these warnings go away.
1521 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1523 NSMenuItem *def_item = nil;
1524 float max_width = 0;
1526 # else // USE_IPHONE
1528 NSString *def_item = nil;
1530 rect.size.width = 0;
1531 rect.size.height = 0;
1532 # ifdef USE_PICKER_VIEW
1533 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1534 popup.delegate = self;
1535 popup.dataSource = self;
1536 # endif // !USE_PICKER_VIEW
1537 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1539 # endif // USE_IPHONE
1541 for (i = 0; i < count; i++) {
1542 NSXMLNode *child = [children objectAtIndex:i];
1544 if ([child kind] == NSXMLCommentKind)
1546 if ([child kind] != NSXMLElementKind) {
1547 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1551 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1553 NSMutableDictionary *dict2 = [@{ @"id": @"",
1557 [self parseAttrs:dict2 node:child];
1558 NSString *label = [dict2 objectForKey:@"_label"];
1559 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1562 NSAssert1 (0, @"no _label in %@", [child name]);
1567 // create the menu item (and then get a pointer to it)
1568 [popup addItemWithTitle:label];
1569 NSMenuItem *item = [popup itemWithTitle:label];
1570 # endif // USE_IPHONE
1573 NSString *this_val = NULL;
1574 NSString *this_key = [self switchToResource: arg_set
1577 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1578 if (menu_key && ![menu_key isEqualToString:this_key])
1580 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1581 menu_key, this_key, this_val);
1583 menu_key = this_key;
1585 /* If this menu has the cmd line "-mode foo" then set this item's
1586 value to "foo" (the menu itself will be bound to e.g. "modeString")
1589 set_menu_item_object (item, this_val);
1591 // Array holds ["Label", "resource-key", "resource-val"].
1592 [items addObject:[NSMutableArray arrayWithObjects:
1593 label, @"", this_val, nil]];
1597 // no arg-set -- only one menu item can be missing that.
1598 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1603 // Array holds ["Label", "resource-key", "resource-val"].
1604 [items addObject:[NSMutableArray arrayWithObjects:
1605 label, @"", @"", nil]];
1609 /* make sure the menu button has room for the text of this item,
1610 and remember the greatest width it has reached.
1613 [popup setTitle:label];
1615 NSRect r = [popup frame];
1616 if (r.size.width > max_width) max_width = r.size.width;
1617 # endif // USE_IPHONE
1621 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1625 /* We've added all of the menu items. If there was an item with no
1626 command-line switch, then it's the item that represents the default
1627 value. Now we must bind to that item as well... (We have to bind
1628 this one late, because if it was the first item, then we didn't
1629 yet know what resource was associated with this menu.)
1632 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1634 @"no default value for resource \"%@\" in menu item \"%@\"",
1644 set_menu_item_object (def_item, def_obj);
1645 # else // !USE_IPHONE
1646 for (NSMutableArray *a in items) {
1647 // Make sure each array contains the resource key.
1648 [a replaceObjectAtIndex:1 withObject:menu_key];
1649 // Make sure the default item contains the default resource value.
1650 if (def_obj && def_item &&
1651 [def_item isEqualToString:[a objectAtIndex:0]])
1652 [a replaceObjectAtIndex:2 withObject:def_obj];
1654 # endif // !USE_IPHONE
1658 # ifdef USE_PICKER_VIEW
1659 /* Finish tweaking the menu button itself.
1662 [popup setTitle:[def_item title]];
1663 NSRect r = [popup frame];
1664 r.size.width = max_width;
1666 # endif // USE_PICKER_VIEW
1669 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1670 [self placeChild:popup on:parent];
1671 [self bindResource:popup key:menu_key];
1676 # ifdef USE_PICKER_VIEW
1677 // Store the items for this picker in the picker_values array.
1678 // This is so fucking stupid.
1680 int menu_number = [pref_keys count] - 1;
1681 if (! picker_values)
1682 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1683 while ([picker_values count] <= menu_number)
1684 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1685 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1686 [popup reloadAllComponents];
1688 # else // !USE_PICKER_VIEW
1690 [self placeSeparator];
1693 for (__attribute__((unused)) NSArray *item in items) {
1694 RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1696 [b setLineBreakMode:NSLineBreakByTruncatingHead];
1697 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1698 [self placeChild:b on:parent];
1702 [self placeSeparator];
1704 # endif // !USE_PICKER_VIEW
1705 # endif // !USE_IPHONE
1710 /* Creates an uneditable, wrapping NSTextField to display the given
1711 text enclosed by <description> ... </description> in the XML.
1713 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1715 NSString *text = nil;
1716 NSArray *children = [node children];
1717 NSUInteger i, count = [children count];
1719 for (i = 0; i < count; i++) {
1720 NSXMLNode *child = [children objectAtIndex:i];
1721 NSString *s = [child objectValue];
1723 text = [text stringByAppendingString:s];
1728 text = unwrap (text);
1730 NSRect rect = [parent frame];
1731 rect.origin.x = rect.origin.y = 0;
1732 rect.size.width = 200;
1733 rect.size.height = 50; // sized later
1735 NSText *lab = [[NSText alloc] initWithFrame:rect];
1736 [lab setEditable:NO];
1737 [lab setDrawsBackground:NO];
1738 [lab setHorizontallyResizable:YES];
1739 [lab setVerticallyResizable:YES];
1740 [lab setString:text];
1745 # else // USE_IPHONE
1747 # ifndef USE_HTML_LABELS
1749 UILabel *lab = [self makeLabel:text];
1750 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1753 # else // USE_HTML_LABELS
1754 HTMLLabel *lab = [[HTMLLabel alloc]
1756 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1757 [lab setFrame:rect];
1759 # endif // USE_HTML_LABELS
1761 [self placeSeparator];
1763 # endif // USE_IPHONE
1765 [self placeChild:lab on:parent];
1770 /* Creates the NSTextField described by the given XML node.
1772 - (void) makeTextField: (NSXMLNode *)node
1773 on: (NSView *)parent
1774 withLabel: (BOOL) label_p
1775 horizontal: (BOOL) horiz_p
1777 NSMutableDictionary *dict = [@{ @"id": @"",
1781 [self parseAttrs:dict node:node];
1782 NSString *label = [dict objectForKey:@"_label"];
1783 NSString *arg = [dict objectForKey:@"arg"];
1785 if (!label && label_p) {
1786 NSAssert1 (0, @"no _label in %@", [node name]);
1790 NSAssert1 (arg, @"no arg in %@", label);
1793 rect.origin.x = rect.origin.y = 0;
1794 rect.size.width = rect.size.height = 10;
1796 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1800 // make the default size be around 30 columns; a typical value for
1801 // these text fields is "xscreensaver-text --cols 40".
1803 [txt setStringValue:@"123456789 123456789 123456789 "];
1805 [[txt cell] setWraps:NO];
1806 [[txt cell] setScrollable:YES];
1807 [txt setStringValue:@""];
1809 # else // USE_IPHONE
1811 txt.adjustsFontSizeToFitWidth = YES;
1812 txt.textColor = [UIColor blackColor];
1813 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1814 txt.placeholder = @"";
1815 txt.borderStyle = UITextBorderStyleRoundedRect;
1816 txt.textAlignment = NSTextAlignmentRight;
1817 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1818 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1819 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1820 txt.clearButtonMode = UITextFieldViewModeAlways;
1821 txt.returnKeyType = UIReturnKeyDone;
1822 txt.delegate = self;
1824 [txt setEnabled: YES];
1826 rect.size.height = [txt.font lineHeight] * 1.2;
1827 [txt setFrame:rect];
1829 # endif // USE_IPHONE
1832 LABEL *lab = [self makeLabel:label];
1833 [self placeChild:lab on:parent];
1837 [self placeChild:txt on:parent right:(label ? YES : NO)];
1839 [self bindSwitch:txt cmdline:arg];
1844 /* Creates the NSTextField described by the given XML node,
1845 and hooks it up to a Choose button and a file selector widget.
1847 - (void) makeFileSelector: (NSXMLNode *)node
1848 on: (NSView *)parent
1849 dirsOnly: (BOOL) dirsOnly
1850 withLabel: (BOOL) label_p
1851 editable: (BOOL) editable_p
1853 # ifndef USE_IPHONE // No files. No selectors.
1854 NSMutableDictionary *dict = [@{ @"id": @"",
1858 [self parseAttrs:dict node:node];
1859 NSString *label = [dict objectForKey:@"_label"];
1860 NSString *arg = [dict objectForKey:@"arg"];
1862 if (!label && label_p) {
1863 NSAssert1 (0, @"no _label in %@", [node name]);
1867 NSAssert1 (arg, @"no arg in %@", label);
1870 rect.origin.x = rect.origin.y = 0;
1871 rect.size.width = rect.size.height = 10;
1873 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1875 // make the default size be around 20 columns.
1877 [txt setStringValue:@"123456789 123456789 "];
1879 [txt setSelectable:YES];
1880 [txt setEditable:editable_p];
1881 [txt setBezeled:editable_p];
1882 [txt setDrawsBackground:editable_p];
1883 [[txt cell] setWraps:NO];
1884 [[txt cell] setScrollable:YES];
1885 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1886 [txt setStringValue:@""];
1890 lab = [self makeLabel:label];
1891 [self placeChild:lab on:parent];
1895 [self placeChild:txt on:parent right:(label ? YES : NO)];
1897 [self bindSwitch:txt cmdline:arg];
1900 // Make the text field and label be the same height, whichever is taller.
1903 rect.size.height = ([lab frame].size.height > [txt frame].size.height
1904 ? [lab frame].size.height
1905 : [txt frame].size.height);
1906 [txt setFrame:rect];
1909 // Now put a "Choose" button next to it.
1911 rect.origin.x = rect.origin.y = 0;
1912 rect.size.width = rect.size.height = 10;
1913 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
1914 [choose setTitle:@"Choose..."];
1915 [choose setBezelStyle:NSRoundedBezelStyle];
1918 [self placeChild:choose on:parent right:YES];
1920 // center the Choose button around the midpoint of the text field.
1921 rect = [choose frame];
1922 rect.origin.y = ([txt frame].origin.y +
1923 (([txt frame].size.height - rect.size.height) / 2));
1924 [choose setFrameOrigin:rect.origin];
1926 [choose setTarget:[parent window]];
1928 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
1930 [choose setAction:@selector(fileSelectorChooseAction:)];
1933 # endif // !USE_IPHONE
1939 /* Runs a modal file selector and sets the text field's value to the
1940 selected file or directory.
1943 do_file_selector (NSTextField *txt, BOOL dirs_p)
1945 NSOpenPanel *panel = [NSOpenPanel openPanel];
1946 [panel setAllowsMultipleSelection:NO];
1947 [panel setCanChooseFiles:!dirs_p];
1948 [panel setCanChooseDirectories:dirs_p];
1950 NSString *file = [txt stringValue];
1951 if ([file length] <= 0) {
1952 file = NSHomeDirectory();
1954 file = [file stringByAppendingPathComponent:@"Pictures"];
1957 // NSString *dir = [file stringByDeletingLastPathComponent];
1959 int result = [panel runModalForDirectory:file //dir
1960 file:nil //[file lastPathComponent]
1962 if (result == NSOKButton) {
1963 NSArray *files = [panel filenames];
1964 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
1965 file = [file stringByAbbreviatingWithTildeInPath];
1966 [txt setStringValue:file];
1968 // Fuck me! Just setting the value of the NSTextField does not cause
1969 // that to end up in the preferences!
1971 NSDictionary *dict = [txt infoForBinding:@"value"];
1972 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
1973 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
1974 if ([path hasPrefix:@"values."]) // WTF.
1975 path = [path substringFromIndex:7];
1976 [[prefs values] setValue:file forKey:path];
1979 // make sure the end of the string is visible.
1980 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
1982 range.location = [file length]-3;
1984 if (! [[txt window] makeFirstResponder:[txt window]])
1985 [[txt window] endEditingFor:nil];
1986 // [[txt window] makeFirstResponder:nil];
1987 [fe setSelectedRange:range];
1988 // [tv scrollRangeToVisible:range];
1989 // [txt setNeedsDisplay:YES];
1990 // [[txt cell] setNeedsDisplay:YES];
1991 // [txt selectAll:txt];
1997 /* Returns the NSTextField that is to the left of or above the NSButton.
1999 static NSTextField *
2000 find_text_field_of_button (NSButton *button)
2002 NSView *parent = [button superview];
2003 NSArray *kids = [parent subviews];
2004 int nkids = [kids count];
2007 for (i = 0; i < nkids; i++) {
2008 NSObject *kid = [kids objectAtIndex:i];
2009 if ([kid isKindOfClass:[NSTextField class]]) {
2010 f = (NSTextField *) kid;
2011 } else if (kid == button) {
2020 - (void) fileSelectorChooseAction:(NSObject *)arg
2022 NSButton *choose = (NSButton *) arg;
2023 NSTextField *txt = find_text_field_of_button (choose);
2024 do_file_selector (txt, NO);
2027 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2029 NSButton *choose = (NSButton *) arg;
2030 NSTextField *txt = find_text_field_of_button (choose);
2031 do_file_selector (txt, YES);
2034 #endif // !USE_IPHONE
2037 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2042 (x) Computer name and time
2043 ( ) Text [__________________________]
2044 ( ) Text file [_________________] [Choose]
2045 ( ) URL [__________________________]
2046 ( ) Shell Cmd [__________________________]
2048 textMode -text-mode date
2049 textMode -text-mode literal textLiteral -text-literal %
2050 textMode -text-mode file textFile -text-file %
2051 textMode -text-mode url textURL -text-url %
2052 textMode -text-mode program textProgram -text-program %
2055 rect.size.width = rect.size.height = 1;
2056 rect.origin.x = rect.origin.y = 0;
2057 NSView *group = [[NSView alloc] initWithFrame:rect];
2058 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2060 Bool program_p = TRUE;
2065 // This is how you link radio buttons together.
2067 NSButtonCell *proto = [[NSButtonCell alloc] init];
2068 [proto setButtonType:NSRadioButton];
2070 rect.origin.x = rect.origin.y = 0;
2071 rect.size.width = rect.size.height = 10;
2072 NSMatrix *matrix = [[NSMatrix alloc]
2074 mode:NSRadioModeMatrix
2076 numberOfRows: 4 + (program_p ? 1 : 0)
2078 [matrix setAllowsEmptySelection:NO];
2080 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
2081 [cnames addObject:@"Computer name and time"];
2082 [cnames addObject:@"Text"];
2083 [cnames addObject:@"File"];
2084 [cnames addObject:@"URL"];
2085 if (program_p) [cnames addObject:@"Shell Cmd"];
2086 [matrix bind:@"content"
2088 withKeyPath:@"arrangedObjects"
2092 [self bindSwitch:matrix cmdline:@"-text-mode %"];
2094 [self placeChild:matrix on:group];
2095 [self placeChild:rgroup on:group right:YES];
2099 # else // USE_IPHONE
2101 NSView *rgroup = parent;
2104 // <select id="textMode">
2105 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
2106 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
2107 // <option id="url" _label="Display URL"/>
2110 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2111 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2113 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2114 [node3 setAttributesAsDictionary:
2116 @"arg-set": @"-text-mode date",
2117 @"_label": @"Display the date and time" }];
2118 [node3 setParent: node2];
2121 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2122 [node3 setAttributesAsDictionary:
2124 @"arg-set": @"-text-mode literal",
2125 @"_label": @"Display static text" }];
2126 [node3 setParent: node2];
2129 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2130 [node3 setAttributesAsDictionary:
2132 @"_label": @"Display the contents of a URL" }];
2133 [node3 setParent: node2];
2136 [self makeOptionMenu:node2 on:rgroup];
2138 # endif // USE_IPHONE
2141 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2142 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2143 [node2 setAttributesAsDictionary:
2144 @{ @"id": @"textLiteral",
2145 @"arg": @"-text-literal %",
2147 @"_label": @"Text to display"
2150 [self makeTextField:node2 on:rgroup
2158 // rect = [last_child(rgroup) frame];
2160 /* // trying to make the text fields be enabled only when the checkbox is on..
2161 control = last_child (rgroup);
2162 [control bind:@"enabled"
2163 toObject:[matrix cellAtRow:1 column:0]
2164 withKeyPath:@"value"
2170 // <file id="textFile" _label="" arg-set="-text-file %"/>
2171 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2172 [node2 setAttributesAsDictionary:
2173 @{ @"id": @"textFile",
2174 @"arg": @"-text-file %" }];
2175 [self makeFileSelector:node2 on:rgroup
2176 dirsOnly:NO withLabel:NO editable:NO];
2177 # endif // !USE_IPHONE
2179 // rect = [last_child(rgroup) frame];
2181 // <string id="textURL" _label="" arg-set="text-url %"/>
2182 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2183 [node2 setAttributesAsDictionary:
2184 @{ @"id": @"textURL",
2185 @"arg": @"-text-url %",
2187 @"_label": @"URL to display",
2190 [self makeTextField:node2 on:rgroup
2198 // rect = [last_child(rgroup) frame];
2202 // <string id="textProgram" _label="" arg-set="text-program %"/>
2203 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2204 [node2 setAttributesAsDictionary:
2205 @{ @"id": @"textProgram",
2206 @"arg": @"-text-program %",
2208 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2211 // rect = [last_child(rgroup) frame];
2213 layout_group (rgroup, NO);
2215 rect = [rgroup frame];
2216 rect.size.width += 35; // WTF? Why is rgroup too narrow?
2217 [rgroup setFrame:rect];
2220 // Set the height of the cells in the radio-box matrix to the height of
2221 // the (last of the) text fields.
2222 control = last_child (rgroup);
2223 rect = [control frame];
2224 rect.size.width = 30; // width of the string "Text", plus a bit...
2226 rect.size.width += 25;
2227 rect.size.height += LINE_SPACING;
2228 [matrix setCellSize:rect.size];
2229 [matrix sizeToCells];
2231 layout_group (group, YES);
2232 rect = [matrix frame];
2233 rect.origin.x += rect.size.width + COLUMN_SPACING;
2234 rect.origin.y -= [control frame].size.height - LINE_SPACING;
2235 [rgroup setFrameOrigin:rect.origin];
2237 // now cheat on the size of the matrix: allow it to overlap (underlap)
2240 rect.size = [matrix cellSize];
2241 rect.size.width = 300;
2242 [matrix setCellSize:rect.size];
2243 [matrix sizeToCells];
2245 // Cheat on the position of the stuff on the right (the rgroup).
2246 // GAAAH, this code is such crap!
2247 rect = [rgroup frame];
2249 [rgroup setFrame:rect];
2252 rect.size.width = rect.size.height = 0;
2253 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2254 [box setTitlePosition:NSAtTop];
2255 [box setBorderType:NSBezelBorder];
2256 [box setTitle:@"Display Text"];
2258 rect.size.width = rect.size.height = 12;
2259 [box setContentViewMargins:rect.size];
2260 [box setContentView:group];
2263 [self placeChild:box on:parent];
2265 # endif // !USE_IPHONE
2269 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2272 [x] Grab desktop images
2273 [ ] Choose random image:
2274 [__________________________] [Choose]
2276 <boolean id="grabDesktopImages" _label="Grab desktop images"
2277 arg-unset="-no-grab-desktop"/>
2278 <boolean id="chooseRandomImages" _label="Grab desktop images"
2279 arg-unset="-choose-random-images"/>
2280 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2283 NSXMLElement *node2;
2286 # define SCREENS "Grab desktop images"
2287 # define PHOTOS "Choose random images"
2289 # define SCREENS "Grab screenshots"
2290 # define PHOTOS "Use photo library"
2293 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2294 [node2 setAttributesAsDictionary:
2295 @{ @"id": @"grabDesktopImages",
2296 @"_label": @ SCREENS,
2297 @"arg-unset": @"-no-grab-desktop",
2299 [self makeCheckbox:node2 on:parent];
2301 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2302 [node2 setAttributesAsDictionary:
2303 @{ @"id": @"chooseRandomImages",
2304 @"_label": @ PHOTOS,
2305 @"arg-set": @"-choose-random-images",
2307 [self makeCheckbox:node2 on:parent];
2309 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2310 [node2 setAttributesAsDictionary:
2311 @{ @"id": @"imageDirectory",
2312 @"_label": @"Images from:",
2313 @"arg": @"-image-directory %",
2315 [self makeFileSelector:node2 on:parent
2316 dirsOnly:YES withLabel:YES editable:YES];
2322 // Add a second, explanatory label below the file/URL selector.
2325 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2326 [self placeChild:lab2 on:parent];
2328 // Pack it in a little tighter vertically.
2329 NSRect r2 = [lab2 frame];
2332 [lab2 setFrameOrigin:r2.origin];
2334 # endif // USE_IPHONE
2338 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2342 [x] Check for Updates [ Monthly ]
2345 <boolean id="automaticallyChecksForUpdates"
2346 _label="Automatically check for updates"
2347 arg-unset="-no-automaticallyChecksForUpdates" />
2348 <select id="updateCheckInterval">
2349 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2350 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/>
2351 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2352 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2360 rect.size.width = rect.size.height = 1;
2361 rect.origin.x = rect.origin.y = 0;
2362 NSView *group = [[NSView alloc] initWithFrame:rect];
2364 NSXMLElement *node2;
2368 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2369 [node2 setAttributesAsDictionary:
2370 @{ @"id": @SUSUEnableAutomaticChecksKey,
2371 @"_label": @"Automatically check for updates",
2372 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2374 [self makeCheckbox:node2 on:group];
2378 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2379 [node2 setAttributesAsDictionary:
2380 @{ @"id": @SUScheduledCheckIntervalKey }];
2384 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2385 [node3 setAttributesAsDictionary:
2386 @{ @"id": @"hourly",
2387 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2388 @"_label": @"Hourly" }];
2389 [node3 setParent: node2];
2392 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2393 [node3 setAttributesAsDictionary:
2395 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2396 @"_label": @"Daily" }];
2397 [node3 setParent: node2];
2400 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2401 [node3 setAttributesAsDictionary:
2402 @{ @"id": @"weekly",
2403 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2404 @"_label": @"Weekly",
2406 [node3 setParent: node2];
2409 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2410 [node3 setAttributesAsDictionary:
2411 @{ @"id": @"monthly",
2412 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2413 @"_label": @"Monthly",
2415 [node3 setParent: node2];
2419 [self makeOptionMenu:node2 on:group];
2422 layout_group (group, TRUE);
2424 rect.size.width = rect.size.height = 0;
2425 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2426 [box setTitlePosition:NSNoTitle];
2427 [box setBorderType:NSNoBorder];
2428 [box setContentViewMargins:rect.size];
2429 [box setContentView:group];
2432 [self placeChild:box on:parent];
2437 # endif // !USE_IPHONE
2441 #pragma mark Layout for controls
2446 last_child (NSView *parent)
2448 NSArray *kids = [parent subviews];
2449 int nkids = [kids count];
2453 return [kids objectAtIndex:nkids-1];
2455 #endif // USE_IPHONE
2458 /* Add the child as a subview of the parent, positioning it immediately
2459 below or to the right of the previously-added child of that view.
2461 - (void) placeChild:
2467 on:(NSView *)parent right:(BOOL)right_p
2470 NSRect rect = [child frame];
2471 NSView *last = last_child (parent);
2473 rect.origin.x = LEFT_MARGIN;
2474 rect.origin.y = ([parent frame].size.height - rect.size.height
2476 } else if (right_p) {
2477 rect = [last frame];
2478 rect.origin.x += rect.size.width + COLUMN_SPACING;
2480 rect = [last frame];
2481 rect.origin.x = LEFT_MARGIN;
2482 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2484 NSRect r = [child frame];
2485 r.origin = rect.origin;
2487 [parent addSubview:child];
2489 # else // USE_IPHONE
2491 // Controls is an array of arrays of the controls, divided into sections.
2493 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2494 if ([controls count] == 0)
2495 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2496 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2498 if (!right_p || [current count] == 0) {
2499 // Nothing on the current line. Add this object.
2500 [current addObject: child];
2502 // Something's on the current line already.
2503 NSObject *old = [current objectAtIndex:[current count]-1];
2504 if ([old isKindOfClass:[NSMutableArray class]]) {
2505 // Already an array in this cell. Append.
2506 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2507 [(NSMutableArray *) old addObject: child];
2509 // Replace the control in this cell with an array, then app
2510 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2511 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2514 # endif // USE_IPHONE
2518 - (void) placeChild:(NSView *)child on:(NSView *)parent
2520 [self placeChild:child on:parent right:NO];
2526 // Start putting subsequent children in a new group, to create a new
2527 // section on the UITableView.
2529 - (void) placeSeparator
2531 if (! controls) return;
2532 if ([controls count] == 0) return;
2533 if ([[controls objectAtIndex:[controls count]-1]
2535 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2537 #endif // USE_IPHONE
2541 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2542 wrapped in <hgroup> or <vgroup> in the XML.
2544 - (void) makeGroup:(NSXMLNode *)node
2546 horizontal:(BOOL) horiz_p
2549 if (!horiz_p) [self placeSeparator];
2550 [self traverseChildren:node on:parent];
2551 if (!horiz_p) [self placeSeparator];
2552 # else // !USE_IPHONE
2554 rect.size.width = rect.size.height = 1;
2555 rect.origin.x = rect.origin.y = 0;
2556 NSView *group = [[NSView alloc] initWithFrame:rect];
2557 [self traverseChildren:node on:group];
2559 layout_group (group, horiz_p);
2561 rect.size.width = rect.size.height = 0;
2562 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2563 [box setTitlePosition:NSNoTitle];
2564 [box setBorderType:NSNoBorder];
2565 [box setContentViewMargins:rect.size];
2566 [box setContentView:group];
2569 [self placeChild:box on:parent];
2570 # endif // !USE_IPHONE
2576 layout_group (NSView *group, BOOL horiz_p)
2578 NSArray *kids = [group subviews];
2579 int nkids = [kids count];
2581 double maxx = 0, miny = 0;
2582 for (i = 0; i < nkids; i++) {
2583 NSView *kid = [kids objectAtIndex:i];
2584 NSRect r = [kid frame];
2587 maxx += r.size.width + COLUMN_SPACING;
2588 if (r.size.height > -miny) miny = -r.size.height;
2590 if (r.size.width > maxx) maxx = r.size.width;
2591 miny = r.origin.y - r.size.height;
2598 rect.size.width = maxx;
2599 rect.size.height = -miny;
2600 [group setFrame:rect];
2603 for (i = 0; i < nkids; i++) {
2604 NSView *kid = [kids objectAtIndex:i];
2605 NSRect r = [kid frame];
2607 r.origin.y = rect.size.height - r.size.height;
2609 x += r.size.width + COLUMN_SPACING;
2616 #endif // !USE_IPHONE
2619 /* Create some kind of control corresponding to the given XML node.
2621 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2623 NSString *name = [node name];
2625 if ([node kind] == NSXMLCommentKind)
2628 if ([node kind] == NSXMLTextKind) {
2629 NSString *s = [(NSString *) [node objectValue]
2630 stringByTrimmingCharactersInSet:
2631 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2632 if (! [s isEqualToString:@""]) {
2633 NSAssert1 (0, @"unexpected text: %@", s);
2638 if ([node kind] != NSXMLElementKind) {
2639 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2643 if ([name isEqualToString:@"hgroup"] ||
2644 [name isEqualToString:@"vgroup"]) {
2646 [self makeGroup:node on:parent
2647 horizontal:[name isEqualToString:@"hgroup"]];
2649 } else if ([name isEqualToString:@"command"]) {
2650 // do nothing: this is the "-root" business
2652 } else if ([name isEqualToString:@"boolean"]) {
2653 [self makeCheckbox:node on:parent];
2655 } else if ([name isEqualToString:@"string"]) {
2656 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2658 } else if ([name isEqualToString:@"file"]) {
2659 [self makeFileSelector:node on:parent
2660 dirsOnly:NO withLabel:YES editable:NO];
2662 } else if ([name isEqualToString:@"number"]) {
2663 [self makeNumberSelector:node on:parent];
2665 } else if ([name isEqualToString:@"select"]) {
2666 [self makeOptionMenu:node on:parent];
2668 } else if ([name isEqualToString:@"_description"]) {
2669 [self makeDescLabel:node on:parent];
2671 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2672 [self makeTextLoaderControlBox:node on:parent];
2674 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2675 [self makeImageLoaderControlBox:node on:parent];
2677 } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2678 [self makeUpdaterControlBox:node on:parent];
2681 NSAssert1 (0, @"unknown tag: %@", name);
2686 /* Iterate over and process the children of this XML node.
2688 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2690 NSArray *children = [node children];
2691 NSUInteger i, count = [children count];
2692 for (i = 0; i < count; i++) {
2693 NSXMLNode *child = [children objectAtIndex:i];
2694 [self makeControl:child on:parent];
2701 /* Kludgey magic to make the window enclose the controls we created.
2704 fix_contentview_size (NSView *parent)
2707 NSArray *kids = [parent subviews];
2708 int nkids = [kids count];
2709 NSView *text = 0; // the NSText at the bottom of the window
2710 double maxx = 0, miny = 0;
2713 /* Find the size of the rectangle taken up by each of the children
2714 except the final "NSText" child.
2716 for (i = 0; i < nkids; i++) {
2717 NSView *kid = [kids objectAtIndex:i];
2718 if ([kid isKindOfClass:[NSText class]]) {
2723 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2724 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2725 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2726 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2727 // f.origin.y + f.size.height, [kid class]);
2730 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2732 /* Now that we know the width of the window, set the width of the NSText to
2733 that, so that it can decide what its height needs to be.
2735 if (! text) abort();
2737 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2738 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2739 // f.origin.y + f.size.height, [text class]);
2741 // set the NSText's width (this changes its height).
2742 f.size.width = maxx - LEFT_MARGIN;
2745 // position the NSText below the last child (this gives us a new miny).
2747 f.origin.y = miny - f.size.height - LINE_SPACING;
2748 miny = f.origin.y - LINE_SPACING;
2751 // Lock the width of the field and unlock the height, and let it resize
2752 // once more, to compute the proper height of the text for that width.
2754 [(NSText *) text setHorizontallyResizable:NO];
2755 [(NSText *) text setVerticallyResizable:YES];
2756 [(NSText *) text sizeToFit];
2758 // Now lock the height too: no more resizing this text field.
2760 [(NSText *) text setVerticallyResizable:NO];
2762 // Now reposition the top edge of the text field to be back where it
2763 // was before we changed the height.
2765 float oh = f.size.height;
2767 float dh = f.size.height - oh;
2770 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2771 // If we do this in 10.6, the text field moves down, off the window.
2772 // So instead we repair it at the end, at the "WTF2" comment.
2775 // Also adjust the parent height by the change in height of the text field.
2778 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2779 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2780 // f.origin.y + f.size.height, [text class]);
2783 /* Set the contentView to the size of the children.
2786 // float yoff = f.size.height;
2787 f.size.width = maxx + LEFT_MARGIN;
2788 f.size.height = -(miny - LEFT_MARGIN*2);
2789 // yoff = f.size.height - yoff;
2790 [parent setFrame:f];
2792 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2793 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2795 /* Now move all of the kids up into the window.
2798 float shift = f.size.height;
2799 // NSLog(@"shift: %3.0f", shift);
2800 for (i = 0; i < nkids; i++) {
2801 NSView *kid = [kids objectAtIndex:i];
2803 f.origin.y += shift;
2805 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2806 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2807 // f.origin.y + f.size.height, [kid class]);
2812 parent: 420 x 541 @ 0 0
2813 text: 380 x 100 @ 20 22 miny=-501
2816 parent: 420 x 541 @ 0 0
2817 text: 380 x 100 @ 20 50 miny=-501
2820 // #### WTF2: See "WTF" above. If the text field is off the screen,
2821 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2824 if (f.origin.y < 50) { // magic numbers, yay
2829 /* Set the kids to track the top left corner of the window when resized.
2830 Set the NSText to track the bottom right corner as well.
2832 for (i = 0; i < nkids; i++) {
2833 NSView *kid = [kids objectAtIndex:i];
2834 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2835 if ([kid isKindOfClass:[NSText class]])
2836 mask |= NSViewWidthSizable|NSViewHeightSizable;
2837 [kid setAutoresizingMask:mask];
2840 # endif // !USE_IPHONE
2846 wrap_with_buttons (NSWindow *window, NSView *panel)
2850 // Make a box to hold the buttons at the bottom of the window.
2852 rect = [panel frame];
2853 rect.origin.x = rect.origin.y = 0;
2854 rect.size.height = 10;
2855 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2856 [bbox setTitlePosition:NSNoTitle];
2857 [bbox setBorderType:NSNoBorder];
2859 // Make some buttons: Default, Cancel, OK
2861 rect.origin.x = rect.origin.y = 0;
2862 rect.size.width = rect.size.height = 10;
2863 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2864 [reset setTitle:@"Reset to Defaults"];
2865 [reset setBezelStyle:NSRoundedBezelStyle];
2868 rect = [reset frame];
2869 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2870 [ok setTitle:@"OK"];
2871 [ok setBezelStyle:NSRoundedBezelStyle];
2873 rect = [bbox frame];
2874 rect.origin.x = rect.size.width - [ok frame].size.width;
2875 [ok setFrameOrigin:rect.origin];
2878 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2879 [cancel setTitle:@"Cancel"];
2880 [cancel setBezelStyle:NSRoundedBezelStyle];
2882 rect.origin.x -= [cancel frame].size.width + 10;
2883 [cancel setFrameOrigin:rect.origin];
2885 // Bind OK to RET and Cancel to ESC.
2886 [ok setKeyEquivalent:@"\r"];
2887 [cancel setKeyEquivalent:@"\e"];
2889 // The correct width for OK and Cancel buttons is 68 pixels
2890 // ("Human Interface Guidelines: Controls: Buttons:
2891 // Push Button Specifications").
2894 rect.size.width = 68;
2897 rect = [cancel frame];
2898 rect.size.width = 68;
2899 [cancel setFrame:rect];
2901 // It puts the buttons in the box or else it gets the hose again
2903 [bbox addSubview:ok];
2904 [bbox addSubview:cancel];
2905 [bbox addSubview:reset];
2908 // make a box to hold the button-box, and the preferences view
2910 rect = [bbox frame];
2911 rect.origin.y += rect.size.height;
2912 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
2913 [pbox setTitlePosition:NSNoTitle];
2914 [pbox setBorderType:NSBezelBorder];
2916 // Enforce a max height on the dialog, so that it's obvious to me
2917 // (on a big screen) when the dialog will fall off the bottom of
2918 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
2920 NSRect f = [panel frame];
2921 int screen_height = (768 // shortest "modern" Mac display
2923 - 56 // System Preferences toolbar
2924 - 140 // default magnified bottom dock icon
2926 if (f.size.height > screen_height) {
2927 NSLog(@"%@ height was %.0f; clipping to %d",
2928 [panel class], f.size.height, screen_height);
2929 f.size.height = screen_height;
2934 [pbox addSubview:panel];
2935 [pbox addSubview:bbox];
2938 [reset setAutoresizingMask:NSViewMaxXMargin];
2939 [cancel setAutoresizingMask:NSViewMinXMargin];
2940 [ok setAutoresizingMask:NSViewMinXMargin];
2941 [bbox setAutoresizingMask:NSViewWidthSizable];
2945 [ok setTarget:window];
2946 [cancel setTarget:window];
2947 [reset setTarget:window];
2948 [ok setAction:@selector(okAction:)];
2949 [cancel setAction:@selector(cancelAction:)];
2950 [reset setAction:@selector(resetAction:)];
2956 #endif // !USE_IPHONE
2959 /* Iterate over and process the children of the root node of the XML document.
2961 - (void)traverseTree
2964 NSView *parent = [self view];
2966 NSWindow *parent = self;
2968 NSXMLNode *node = xml_root;
2970 if (![[node name] isEqualToString:@"screensaver"]) {
2971 NSAssert (0, @"top level node is not <xscreensaver>");
2974 saver_name = [self parseXScreenSaverTag: node];
2975 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
2977 [saver_name retain];
2982 rect.origin.x = rect.origin.y = 0;
2983 rect.size.width = rect.size.height = 1;
2985 NSView *panel = [[NSView alloc] initWithFrame:rect];
2986 [self traverseChildren:node on:panel];
2987 fix_contentview_size (panel);
2989 NSView *root = wrap_with_buttons (parent, panel);
2990 [userDefaultsController setAppliesImmediately:NO];
2991 [globalDefaultsController setAppliesImmediately:NO];
2993 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
2995 rect = [parent frameRectForContentRect:[root frame]];
2996 [parent setFrame:rect display:NO];
2997 [parent setMinSize:rect.size];
2999 [parent setContentView:root];
3004 # else // USE_IPHONE
3006 CGRect r = [parent frame];
3007 r.size = [[UIScreen mainScreen] bounds].size;
3008 [parent setFrame:r];
3009 [self traverseChildren:node on:parent];
3011 # endif // USE_IPHONE
3015 - (void)parser:(NSXMLParser *)parser
3016 didStartElement:(NSString *)elt
3017 namespaceURI:(NSString *)ns
3018 qualifiedName:(NSString *)qn
3019 attributes:(NSDictionary *)attrs
3021 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3022 [e setKind:SimpleXMLElementKind];
3023 [e setAttributesAsDictionary:attrs];
3024 NSXMLElement *p = xml_parsing;
3028 xml_root = xml_parsing;
3031 - (void)parser:(NSXMLParser *)parser
3032 didEndElement:(NSString *)elt
3033 namespaceURI:(NSString *)ns
3034 qualifiedName:(NSString *)qn
3036 NSXMLElement *p = xml_parsing;
3038 NSLog(@"extra close: %@", elt);
3039 } else if (![[p name] isEqualToString:elt]) {
3040 NSLog(@"%@ closed by %@", [p name], elt);
3042 NSXMLElement *n = xml_parsing;
3043 xml_parsing = [n parent];
3048 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3050 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3051 [e setKind:SimpleXMLTextKind];
3052 NSXMLElement *p = xml_parsing;
3054 [e setObjectValue: string];
3059 # ifdef USE_PICKER_VIEW
3061 #pragma mark UIPickerView delegate methods
3063 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3065 return 1; // Columns
3068 - (NSInteger)pickerView:(UIPickerView *)pv
3069 numberOfRowsInComponent:(NSInteger)column
3071 NSAssert (column == 0, @"weird column");
3072 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3073 if (! a) return 0; // Too early?
3077 - (CGFloat)pickerView:(UIPickerView *)pv
3078 rowHeightForComponent:(NSInteger)column
3083 - (CGFloat)pickerView:(UIPickerView *)pv
3084 widthForComponent:(NSInteger)column
3086 NSAssert (column == 0, @"weird column");
3087 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3088 if (! a) return 0; // Too early?
3090 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3092 for (NSArray *a2 in a) {
3093 NSString *s = [a2 objectAtIndex:0];
3094 CGSize r = [s sizeWithFont:f];
3095 if (r.width > max) max = r.width;
3098 max *= 1.7; // WTF!!
3110 - (NSString *)pickerView:(UIPickerView *)pv
3111 titleForRow:(NSInteger)row
3112 forComponent:(NSInteger)column
3114 NSAssert (column == 0, @"weird column");
3115 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3116 if (! a) return 0; // Too early?
3117 a = [a objectAtIndex:row];
3118 NSAssert (a, @"internal error");
3119 return [a objectAtIndex:0];
3122 # endif // USE_PICKER_VIEW
3125 #pragma mark UITableView delegate methods
3127 - (void) addResetButton
3129 [[self navigationItem]
3130 setRightBarButtonItem: [[UIBarButtonItem alloc]
3131 initWithTitle: @"Reset to Defaults"
3132 style: UIBarButtonItemStyleBordered
3134 action:@selector(resetAction:)]];
3135 NSString *s = saver_name;
3136 if ([self view].frame.size.width > 320)
3137 s = [s stringByAppendingString: @" Settings"];
3138 [self navigationItem].title = s;
3142 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3147 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3148 // Number of vertically-stacked white boxes.
3149 return [controls count];
3152 - (NSInteger)tableView:(UITableView *)tableView
3153 numberOfRowsInSection:(NSInteger)section
3155 // Number of lines in each vertically-stacked white box.
3156 NSAssert (controls, @"internal error");
3157 return [[controls objectAtIndex:section] count];
3160 - (NSString *)tableView:(UITableView *)tv
3161 titleForHeaderInSection:(NSInteger)section
3163 // Titles above each vertically-stacked white box.
3164 // if (section == 0)
3165 // return [saver_name stringByAppendingString:@" Settings"];
3170 - (CGFloat)tableView:(UITableView *)tv
3171 heightForRowAtIndexPath:(NSIndexPath *)ip
3173 CGFloat h = [tv rowHeight];
3175 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3176 objectAtIndex:[ip indexAtPosition:1]];
3178 if ([ctl isKindOfClass:[NSArray class]]) {
3179 NSArray *set = (NSArray *) ctl;
3180 switch ([set count]) {
3182 # ifdef LABEL_ABOVE_SLIDER
3183 h *= 1.7; break; // label + left/slider/right: 2 1/2 lines
3185 case 3: h *= 1.2; break; // left/slider/right: 1 1/2 lines
3187 if ([[set objectAtIndex:1] isKindOfClass:[UITextField class]])
3191 } else if ([ctl isKindOfClass:[UILabel class]]) {
3192 UILabel *t = (UILabel *) ctl;
3194 r.size.width = 250; // WTF! Black magic!
3195 r.size.width -= LEFT_MARGIN;
3199 h = r.size.height + LINE_SPACING * 3;
3200 # ifdef USE_HTML_LABELS
3202 } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3204 HTMLLabel *t = (HTMLLabel *) ctl;
3206 r.size.width = [tv frame].size.width;
3207 r.size.width -= LEFT_MARGIN * 2;
3211 h = r.size.height + LINE_SPACING * 3;
3213 # endif // USE_HTML_LABELS
3215 CGFloat h2 = [ctl frame].size.height;
3216 h2 += LINE_SPACING * 2;
3224 - (void)refreshTableView
3226 UITableView *tv = (UITableView *) [self view];
3227 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3228 NSInteger rows = [self numberOfSectionsInTableView:tv];
3229 for (int i = 0; i < rows; i++) {
3230 NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3231 for (int j = 0; j < cols; j++) {
3235 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3240 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3245 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3247 [NSTimer scheduledTimerWithTimeInterval: 0
3249 selector:@selector(refreshTableView)
3255 #ifndef USE_PICKER_VIEW
3257 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3258 button:(RadioButton *)b
3260 NSArray *item = [[b items] objectAtIndex: [b index]];
3261 NSString *pref_key = [item objectAtIndex:1];
3262 NSObject *pref_val = [item objectAtIndex:2];
3264 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3266 // Convert them both to strings and compare those, so that
3267 // we don't get screwed by int 1 versus string "1".
3268 // Will boolean true/1 screw us here too?
3270 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3271 ? (NSString *) pref_val
3272 : [(NSNumber *) pref_val stringValue]);
3273 NSString *current_str = ([current isKindOfClass:[NSString class]]
3274 ? (NSString *) current
3275 : [(NSNumber *) current stringValue]);
3276 BOOL match_p = [current_str isEqualToString:pref_str];
3278 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3281 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3283 [cell setAccessoryType:UITableViewCellAccessoryNone];
3287 - (void)tableView:(UITableView *)tv
3288 didSelectRowAtIndexPath:(NSIndexPath *)ip
3290 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3291 objectAtIndex:[ip indexAtPosition:1]];
3292 if (! [ctl isKindOfClass:[RadioButton class]])
3295 [self radioAction:ctl];
3296 [self refreshTableView];
3300 #endif // !USE_PICKER_VIEW
3304 - (UITableViewCell *)tableView:(UITableView *)tv
3305 cellForRowAtIndexPath:(NSIndexPath *)ip
3308 /* #### If we re-use cells, then clicking on a checkbox RadioButton
3309 (in non-USE_PICKER_VIEW mode) makes all the cells disappear.
3310 This doesn't happen if we don't re-use any cells. Oh well.
3312 NSString *id = [NSString stringWithFormat: @"%d:%d",
3313 [ip indexAtPosition:0],
3314 [ip indexAtPosition:1]];
3315 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier: id];
3317 if (cell) return cell;
3320 UITableViewCell *cell;
3323 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3324 reuseIdentifier: id]
3326 cell.selectionStyle = UITableViewCellSelectionStyleNone;
3328 CGRect p = [cell frame];
3331 p.size.height = [self tableView:tv heightForRowAtIndexPath:ip];
3334 // Allocate more space to the controls on iPad screens,
3335 // and on landscape-mode iPhones.
3336 CGFloat ww = [tv frame].size.width;
3337 CGFloat left_edge = (ww > 700
3338 ? p.size.width * 0.9
3340 ? p.size.width * 0.5
3341 : p.size.width * 0.3);
3342 CGFloat right_edge = p.origin.x + p.size.width - LEFT_MARGIN;
3345 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3346 objectAtIndex:[ip indexAtPosition:1]];
3348 if ([ctl isKindOfClass:[NSArray class]]) {
3349 // This cell has a set of objects in it.
3350 NSArray *set = (NSArray *) ctl;
3351 switch ([set count]) {
3354 // With 2 elements, the first of the pair must be a label.
3355 UILabel *label = (UILabel *) [set objectAtIndex: 0];
3356 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3357 ctl = [set objectAtIndex: 1];
3360 if ([ctl isKindOfClass:[UISwitch class]]) {
3361 // Flush right checkboxes.
3362 ctl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
3363 r.size.width = 80; // Magic.
3364 r.origin.x = right_edge - r.size.width;
3366 // Expandable sliders.
3367 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
3368 r.origin.x = left_edge;
3369 r.size.width = right_edge - r.origin.x;
3371 r.origin.y = (p.size.height - r.size.height) / 2;
3375 NSView *box = [[UIView alloc] initWithFrame:p];
3376 [box addSubview: ctl];
3378 // cell.textLabel.text = [(UILabel *) ctl text];
3380 r.origin.x = LEFT_MARGIN;
3382 r.size.width = [ctl frame].origin.x - r.origin.x;
3383 r.size.height = p.size.height;
3385 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3386 label.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3387 box. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3388 [box addSubview: label];
3396 // With 3 elements, the first and last must be labels.
3397 // With 4 elements, the first, second and last must be labels.
3399 UILabel *top = ([set count] == 4
3400 ? [set objectAtIndex: i++]
3402 UILabel *left = [set objectAtIndex: i++];
3403 NSView *mid = [set objectAtIndex: i++];
3404 UILabel *right = [set objectAtIndex: i++];
3405 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3406 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3407 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3408 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3410 // 3 elements: control at top of cell.
3411 // 4 elements: center the control vertically.
3413 # ifdef LABEL_ABOVE_SLIDER
3414 left_edge = LEFT_MARGIN;
3416 r.origin.x = left_edge;
3417 r.size.width = right_edge - r.origin.x;
3418 r.origin.y = ([set count] == 3
3420 : (p.size.height - r.size.height) / 2);
3423 // Top label goes above, flush center/top.
3425 r.size = [[top text] sizeWithFont:[top font]
3427 CGSizeMake (p.size.width - LEFT_MARGIN*2,
3429 lineBreakMode:[top lineBreakMode]];
3430 r.origin.x = (p.size.width - r.size.width) / 2;
3435 // Left label goes under control, flush left/bottom.
3436 r.size = [[left text] sizeWithFont:[left font]
3438 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3440 lineBreakMode:[left lineBreakMode]];
3441 r.origin.x = [mid frame].origin.x;
3442 r.origin.y = p.size.height - r.size.height - 4;
3445 // Right label goes under control, flush right/bottom.
3447 r.size = [[right text] sizeWithFont:[right font]
3449 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3451 lineBreakMode:[right lineBreakMode]];
3452 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3454 r.origin.y = [left frame].origin.y;
3458 ctl = [[UIView alloc] initWithFrame:p];
3460 # ifdef LABEL_ABOVE_SLIDER
3461 [ctl addSubview: top];
3462 top.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin|
3463 UIViewAutoresizingFlexibleRightMargin);
3466 r.origin.x = LEFT_MARGIN;
3468 r.size.width = [mid frame].origin.x - r.origin.x;
3469 r.size.height = p.size.height;
3471 top.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3472 [ctl addSubview: top];
3475 [ctl addSubview: left];
3476 [ctl addSubview: mid];
3477 [ctl addSubview: right];
3479 left. autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3480 mid. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3481 right.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
3482 ctl. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3486 NSAssert (0, @"unhandled size");
3489 // A single view, not a pair.
3492 r.origin.x = LEFT_MARGIN;
3493 r.origin.y = LINE_SPACING;
3494 r.size.width = right_edge - r.origin.x;
3497 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
3499 # ifndef USE_PICKER_VIEW
3500 if ([ctl isKindOfClass:[RadioButton class]])
3501 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3502 # endif // USE_PICKER_VIEW
3505 if ([ctl isKindOfClass:[UILabel class]]) {
3506 // Make label full height to allow text to line-wrap if necessary.
3508 r.origin.y = p.origin.y;
3509 r.size.height = p.size.height;
3513 [cell.contentView addSubview: ctl];
3517 # endif // USE_IPHONE
3520 /* When this object is instantiated, it parses the XML file and creates
3521 controls on itself that are hooked up to the appropriate preferences.
3522 The default size of the view is just big enough to hold them all.
3524 - (id)initWithXML: (NSData *) xml_data
3525 options: (const XrmOptionDescRec *) _opts
3526 controller: (NSUserDefaultsController *) _prefs
3527 globalController: (NSUserDefaultsController *) _globalPrefs
3528 defaults: (NSDictionary *) _defs
3531 self = [super init];
3532 # else // !USE_IPHONE
3533 self = [super initWithStyle:UITableViewStyleGrouped];
3534 self.title = [saver_name stringByAppendingString:@" Settings"];
3535 # endif // !USE_IPHONE
3536 if (! self) return 0;
3538 // instance variables
3540 defaultOptions = _defs;
3541 userDefaultsController = [_prefs retain];
3542 globalDefaultsController = [_globalPrefs retain];
3544 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3547 NSAssert1 (0, @"XML Error: %@",
3548 [[NSString alloc] initWithData:xml_data
3549 encoding:NSUTF8StringEncoding]);
3552 [xmlDoc setDelegate:self];
3553 if (! [xmlDoc parse]) {
3554 NSError *err = [xmlDoc parserError];
3555 NSAssert2 (0, @"XML Error: %@: %@",
3556 [[NSString alloc] initWithData:xml_data
3557 encoding:NSUTF8StringEncoding],
3562 [self traverseTree];
3566 [self addResetButton];
3575 [saver_name release];
3576 [userDefaultsController release];
3577 [globalDefaultsController release];
3580 [pref_keys release];
3581 [pref_ctls release];
3582 # ifdef USE_PICKER_VIEW
3583 [picker_values release];