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 int nlines = [lines count];
803 text = @"\n"; // start with one blank line
805 // skip trailing blank lines in file
806 for (i = nlines-1; i > 0; i--) {
807 NSString *s = (NSString *) [lines objectAtIndex:i];
813 // skip leading blank lines in file
814 for (i = 0; i < nlines; i++) {
815 NSString *s = (NSString *) [lines objectAtIndex:i];
822 for (; i < nlines; i++) {
823 NSString *s = (NSString *) [lines objectAtIndex:i];
824 if ([s length] == 0) {
825 text = [text stringByAppendingString:@"\n\n"];
827 } else if ([s characterAtIndex:0] == ' ' ||
828 [s hasPrefix:@"Copyright "] ||
829 [s hasPrefix:@"http://"]) {
830 // don't unwrap if the following line begins with whitespace,
831 // or with the word "Copyright", or if it begins with a URL.
833 text = [text stringByAppendingString:@"\n"];
834 text = [text stringByAppendingString:s];
839 text = [text stringByAppendingString:@" "];
840 text = [text stringByAppendingString:s];
851 /* Makes the text up to the first comma be bold.
854 boldify (NSText *nstext)
856 NSString *text = [nstext string];
857 NSRange r = [text rangeOfString:@"," options:0];
858 r.length = r.location+1;
862 NSFont *font = [nstext font];
863 font = [NSFont boldSystemFontOfSize:[font pointSize]];
864 [nstext setFont:font range:r];
866 # endif // !USE_IPHONE
869 /* Creates a human-readable anchor to put on a URL.
872 anchorize (const char *url)
874 const char *wiki = "http://en.wikipedia.org/wiki/";
875 const char *math = "http://mathworld.wolfram.com/";
876 if (!strncmp (wiki, url, strlen(wiki))) {
877 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
878 strcpy (anchor, "Wikipedia: \"");
879 const char *in = url + strlen(wiki);
880 char *out = anchor + strlen(anchor);
884 } else if (*in == '#') {
887 } else if (*in == '%') {
893 sscanf (hex, "%x", &n);
905 } else if (!strncmp (math, url, strlen(math))) {
906 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
907 strcpy (anchor, "MathWorld: \"");
908 const char *start = url + strlen(wiki);
909 const char *in = start;
910 char *out = anchor + strlen(anchor);
914 } else if (in != start && *in >= 'A' && *in <= 'Z') {
917 } else if (!strncmp (in, ".htm", 4)) {
934 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
936 /* Converts any http: URLs in the given text field to clickable links.
939 hreffify (NSText *nstext)
942 NSString *text = [nstext string];
943 [nstext setRichText:YES];
945 NSString *text = [nstext text];
948 int L = [text length];
949 NSRange start; // range is start-of-search to end-of-string
952 while (start.location < L) {
954 // Find the beginning of a URL...
956 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
957 if (r2.location == NSNotFound)
960 // Next time around, start searching after this.
961 start.location = r2.location + r2.length;
962 start.length = L - start.location;
964 // Find the end of a URL (whitespace or EOF)...
966 NSRange r3 = [text rangeOfCharacterFromSet:
967 [NSCharacterSet whitespaceAndNewlineCharacterSet]
968 options:0 range:start];
969 if (r3.location == NSNotFound) // EOF
970 r3.location = L, r3.length = 0;
972 // Next time around, start searching after this.
973 start.location = r3.location;
974 start.length = L - start.location;
976 // Set r2 to the start/length of this URL.
977 r2.length = start.location - r2.location;
980 NSString *nsurl = [text substringWithRange:r2];
981 const char *url = [nsurl UTF8String];
983 // If this is a Wikipedia URL, make the linked text be prettier.
985 char *anchor = anchorize(url);
989 // Construct the RTF corresponding to <A HREF="url">anchor</A>
991 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
992 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
993 sprintf (rtf, fmt, url, anchor);
995 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
996 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
998 # else // !USE_IPHONE
999 // *anchor = 0; // Omit Wikipedia anchor
1000 text = [text stringByReplacingCharactersInRange:r2
1001 withString:[NSString stringWithCString:anchor
1002 encoding:NSUTF8StringEncoding]];
1003 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1004 // withString:@"\n\n"];
1005 # endif // !USE_IPHONE
1009 int L2 = [text length]; // might have changed
1010 start.location -= (L - L2);
1015 [nstext setText:text];
1020 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1024 #pragma mark Creating controls from XML
1027 /* Parse the attributes of an XML tag into a dictionary.
1028 For input, the dictionary should have as attributes the keys, each
1029 with @"" as their value.
1030 On output, the dictionary will set the keys to the values specified,
1031 and keys that were not specified will not be present in the dictionary.
1032 Warnings are printed if there are duplicate or unknown attributes.
1034 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1036 NSArray *attrs = [(NSXMLElement *) node attributes];
1037 int n = [attrs count];
1040 // For each key in the dictionary, fill in the dict with the corresponding
1041 // value. The value @"" is assumed to mean "un-set". Issue a warning if
1042 // an attribute is specified twice.
1044 for (i = 0; i < n; i++) {
1045 NSXMLNode *attr = [attrs objectAtIndex:i];
1046 NSString *key = [attr name];
1047 NSString *val = [attr objectValue];
1048 NSString *old = [dict objectForKey:key];
1051 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1052 } else if ([old length] != 0) {
1053 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1055 [dict setValue:val forKey:key];
1059 // Remove from the dictionary any keys whose value is still @"",
1060 // meaning there was no such attribute specified.
1062 NSArray *keys = [dict allKeys];
1064 for (i = 0; i < n; i++) {
1065 NSString *key = [keys objectAtIndex:i];
1066 NSString *val = [dict objectForKey:key];
1067 if ([val length] == 0)
1068 [dict removeObjectForKey:key];
1072 // Kludge for starwars.xml:
1073 // If there is a "_low-label" and no "_label", but "_low-label" contains
1074 // spaces, divide them.
1075 NSString *lab = [dict objectForKey:@"_label"];
1076 NSString *low = [dict objectForKey:@"_low-label"];
1079 [[[low stringByTrimmingCharactersInSet:
1080 [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1081 componentsSeparatedByString: @" "]
1082 filteredArrayUsingPredicate:
1083 [NSPredicate predicateWithFormat:@"length > 0"]];
1084 if (split && [split count] == 2) {
1085 [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1086 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1089 # endif // USE_IPHONE
1093 /* Handle the options on the top level <xscreensaver> tag.
1095 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1097 NSMutableDictionary *dict = [@{ @"name": @"",
1101 [self parseAttrs:dict node:node];
1102 NSString *name = [dict objectForKey:@"name"];
1103 NSString *label = [dict objectForKey:@"_label"];
1105 NSAssert1 (label, @"no _label in %@", [node name]);
1106 NSAssert1 (name, @"no name in \"%@\"", label);
1111 /* Creates a label: an un-editable NSTextField displaying the given text.
1113 - (LABEL *) makeLabel:(NSString *)text
1116 rect.origin.x = rect.origin.y = 0;
1117 rect.size.width = rect.size.height = 10;
1119 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1120 [lab setSelectable:NO];
1121 [lab setEditable:NO];
1122 [lab setBezeled:NO];
1123 [lab setDrawsBackground:NO];
1124 [lab setStringValue:text];
1126 # else // USE_IPHONE
1127 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1128 [lab setText: [text stringByTrimmingCharactersInSet:
1129 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1130 [lab setBackgroundColor:[UIColor clearColor]];
1131 [lab setNumberOfLines:0]; // unlimited
1132 // [lab setLineBreakMode:UILineBreakModeWordWrap];
1133 [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1134 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1135 UIViewAutoresizingFlexibleHeight)];
1136 # endif // USE_IPHONE
1141 /* Creates the checkbox (NSButton) described by the given XML node.
1143 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1145 NSMutableDictionary *dict = [@{ @"id": @"",
1150 [self parseAttrs:dict node:node];
1151 NSString *label = [dict objectForKey:@"_label"];
1152 NSString *arg_set = [dict objectForKey:@"arg-set"];
1153 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1156 NSAssert1 (0, @"no _label in %@", [node name]);
1159 if (!arg_set && !arg_unset) {
1160 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
1163 if (arg_set && arg_unset) {
1164 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
1168 // sanity-check the choice of argument names.
1170 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1171 [arg_set hasPrefix:@"--no-"]))
1172 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1174 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1175 ![arg_unset hasPrefix:@"--no-"]))
1176 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1180 rect.origin.x = rect.origin.y = 0;
1181 rect.size.width = rect.size.height = 10;
1185 NSButton *button = [[NSButton alloc] initWithFrame:rect];
1186 [button setButtonType:NSSwitchButton];
1187 [button setTitle:label];
1189 [self placeChild:button on:parent];
1191 # else // USE_IPHONE
1193 LABEL *lab = [self makeLabel:label];
1194 [self placeChild:lab on:parent];
1195 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1196 [self placeChild:button on:parent right:YES];
1199 # endif // USE_IPHONE
1201 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1206 /* Creates the number selection control described by the given XML node.
1207 If "type=slider", it's an NSSlider.
1208 If "type=spinbutton", it's a text field with up/down arrows next to it.
1210 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1212 NSMutableDictionary *dict = [@{ @"id": @"",
1215 @"_high-label": @"",
1223 [self parseAttrs:dict node:node];
1224 NSString *label = [dict objectForKey:@"_label"];
1225 NSString *low_label = [dict objectForKey:@"_low-label"];
1226 NSString *high_label = [dict objectForKey:@"_high-label"];
1227 NSString *type = [dict objectForKey:@"type"];
1228 NSString *arg = [dict objectForKey:@"arg"];
1229 NSString *low = [dict objectForKey:@"low"];
1230 NSString *high = [dict objectForKey:@"high"];
1231 NSString *def = [dict objectForKey:@"default"];
1232 NSString *cvt = [dict objectForKey:@"convert"];
1234 NSAssert1 (arg, @"no arg in %@", label);
1235 NSAssert1 (type, @"no type in %@", label);
1238 NSAssert1 (0, @"no low in %@", [node name]);
1242 NSAssert1 (0, @"no high in %@", [node name]);
1246 NSAssert1 (0, @"no default in %@", [node name]);
1249 if (cvt && ![cvt isEqualToString:@"invert"]) {
1250 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1254 // If either the min or max field contains a decimal point, then this
1255 // option may have a floating point value; otherwise, it is constrained
1256 // to be an integer.
1258 NSCharacterSet *dot =
1259 [NSCharacterSet characterSetWithCharactersInString:@"."];
1260 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1261 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1263 if ([type isEqualToString:@"slider"]
1264 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1265 || [type isEqualToString:@"spinbutton"]
1270 rect.origin.x = rect.origin.y = 0;
1271 rect.size.width = 150;
1272 rect.size.height = 23; // apparent min height for slider with ticks...
1274 slider = [[InvertedSlider alloc] initWithFrame:rect
1276 integers: !float_p];
1277 [slider setMaxValue:[high doubleValue]];
1278 [slider setMinValue:[low doubleValue]];
1280 int range = [slider maxValue] - [slider minValue] + 1;
1283 while (range2 > max_ticks)
1286 // If we have elided ticks, leave it at the max number of ticks.
1287 if (range != range2 && range2 < max_ticks)
1290 // If it's a float, always display the max number of ticks.
1291 if (float_p && range2 < max_ticks)
1295 [slider setNumberOfTickMarks:range2];
1297 [slider setAllowsTickMarkValuesOnly:
1298 (range == range2 && // we are showing the actual number of ticks
1299 !float_p)]; // and we want integer results
1300 # endif // !USE_IPHONE
1302 // #### Note: when the slider's range is large enough that we aren't
1303 // showing all possible ticks, the slider's value is not constrained
1304 // to be an integer, even though it should be...
1305 // Maybe we need to use a value converter or something?
1309 lab = [self makeLabel:label];
1310 [self placeChild:lab on:parent];
1313 CGFloat s = [NSFont systemFontSize] + 4;
1314 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1321 lab = [self makeLabel:low_label];
1322 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1324 [lab setAlignment:1]; // right aligned
1326 if (rect.size.width < LEFT_LABEL_WIDTH)
1327 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1328 rect.size.height = [slider frame].size.height;
1329 [lab setFrame:rect];
1330 [self placeChild:lab on:parent];
1331 # else // USE_IPHONE
1332 [lab setTextAlignment: NSTextAlignmentRight];
1333 [self placeChild:lab on:parent right:(label ? YES : NO)];
1334 # endif // USE_IPHONE
1340 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1341 # else // USE_IPHONE
1342 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1343 # endif // USE_IPHONE
1346 // Make left label be same height as slider.
1348 rect.size.height = [slider frame].size.height;
1349 [lab setFrame:rect];
1353 rect = [slider frame];
1354 if (rect.origin.x < LEFT_LABEL_WIDTH)
1355 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1356 [slider setFrame:rect];
1360 lab = [self makeLabel:high_label];
1361 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1364 // Make right label be same height as slider.
1365 rect.size.height = [slider frame].size.height;
1366 [lab setFrame:rect];
1367 [self placeChild:lab on:parent right:YES];
1371 [self bindSwitch:slider cmdline:arg];
1374 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1376 } else if ([type isEqualToString:@"spinbutton"]) {
1379 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1382 NSAssert1 (!low_label,
1383 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1384 NSAssert1 (!high_label,
1385 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1386 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1390 rect.origin.x = rect.origin.y = 0;
1391 rect.size.width = rect.size.height = 10;
1393 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1394 [txt setStringValue:@"0000.0"];
1396 [txt setStringValue:@""];
1399 LABEL *lab = [self makeLabel:label];
1400 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1401 [lab setAlignment:1]; // right aligned
1403 if (rect.size.width < LEFT_LABEL_WIDTH)
1404 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1405 rect.size.height = [txt frame].size.height;
1406 [lab setFrame:rect];
1407 [self placeChild:lab on:parent];
1411 [self placeChild:txt on:parent right:(label ? YES : NO)];
1415 if (rect.origin.x < LEFT_LABEL_WIDTH)
1416 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1417 [txt setFrame:rect];
1420 rect.size.width = rect.size.height = 10;
1421 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1423 [self placeChild:step on:parent right:YES];
1424 rect = [step frame];
1425 rect.origin.x -= COLUMN_SPACING; // this one goes close
1426 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1427 [step setFrame:rect];
1429 [step setMinValue:[low doubleValue]];
1430 [step setMaxValue:[high doubleValue]];
1431 [step setAutorepeat:YES];
1432 [step setValueWraps:NO];
1434 double range = [high doubleValue] - [low doubleValue];
1436 [step setIncrement:range / 10.0];
1437 else if (range >= 500)
1438 [step setIncrement:range / 100.0];
1440 [step setIncrement:1.0];
1442 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1443 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1444 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1445 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1446 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1447 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1448 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1450 [fmt setGeneratesDecimalNumbers:float_p];
1451 [[txt cell] setFormatter:fmt];
1453 [self bindSwitch:step cmdline:arg];
1454 [self bindSwitch:txt cmdline:arg];
1459 # endif // USE_IPHONE
1462 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1469 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1471 /* If the object associated with this menu item looks like a boolean,
1472 store an NSNumber instead of an NSString, since that's what
1473 will be in the preferences (due to similar logic in PrefsReader).
1475 if ([obj isKindOfClass:[NSString class]]) {
1476 NSString *string = (NSString *) obj;
1477 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1478 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1479 obj = [NSNumber numberWithBool:YES];
1480 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1481 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1482 obj = [NSNumber numberWithBool:NO];
1487 [item setRepresentedObject:obj];
1488 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1490 # endif // !USE_IPHONE
1493 /* Creates the popup menu described by the given XML node (and its children).
1495 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1497 NSArray *children = [node children];
1498 int i, count = [children count];
1501 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1505 // get the "id" attribute off the <select> tag.
1507 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1508 [self parseAttrs:dict node:node];
1511 rect.origin.x = rect.origin.y = 0;
1512 rect.size.width = 10;
1513 rect.size.height = 10;
1515 NSString *menu_key = nil; // the resource key used by items in this menu
1518 // #### "Build and Analyze" says that all of our widgets leak, because it
1519 // seems to not realize that placeChild -> addSubview retains them.
1520 // Not sure what to do to make these warnings go away.
1522 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1524 NSMenuItem *def_item = nil;
1525 float max_width = 0;
1527 # else // USE_IPHONE
1529 NSString *def_item = nil;
1531 rect.size.width = 0;
1532 rect.size.height = 0;
1533 # ifdef USE_PICKER_VIEW
1534 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1535 popup.delegate = self;
1536 popup.dataSource = self;
1537 # endif // !USE_PICKER_VIEW
1538 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1540 # endif // USE_IPHONE
1542 for (i = 0; i < count; i++) {
1543 NSXMLNode *child = [children objectAtIndex:i];
1545 if ([child kind] == NSXMLCommentKind)
1547 if ([child kind] != NSXMLElementKind) {
1548 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1552 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1554 NSMutableDictionary *dict2 = [@{ @"id": @"",
1558 [self parseAttrs:dict2 node:child];
1559 NSString *label = [dict2 objectForKey:@"_label"];
1560 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1563 NSAssert1 (0, @"no _label in %@", [child name]);
1568 // create the menu item (and then get a pointer to it)
1569 [popup addItemWithTitle:label];
1570 NSMenuItem *item = [popup itemWithTitle:label];
1571 # endif // USE_IPHONE
1574 NSString *this_val = NULL;
1575 NSString *this_key = [self switchToResource: arg_set
1578 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1579 if (menu_key && ![menu_key isEqualToString:this_key])
1581 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1582 menu_key, this_key, this_val);
1584 menu_key = this_key;
1586 /* If this menu has the cmd line "-mode foo" then set this item's
1587 value to "foo" (the menu itself will be bound to e.g. "modeString")
1590 set_menu_item_object (item, this_val);
1592 // Array holds ["Label", "resource-key", "resource-val"].
1593 [items addObject:[NSMutableArray arrayWithObjects:
1594 label, @"", this_val, nil]];
1598 // no arg-set -- only one menu item can be missing that.
1599 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1604 // Array holds ["Label", "resource-key", "resource-val"].
1605 [items addObject:[NSMutableArray arrayWithObjects:
1606 label, @"", @"", nil]];
1610 /* make sure the menu button has room for the text of this item,
1611 and remember the greatest width it has reached.
1614 [popup setTitle:label];
1616 NSRect r = [popup frame];
1617 if (r.size.width > max_width) max_width = r.size.width;
1618 # endif // USE_IPHONE
1622 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1626 /* We've added all of the menu items. If there was an item with no
1627 command-line switch, then it's the item that represents the default
1628 value. Now we must bind to that item as well... (We have to bind
1629 this one late, because if it was the first item, then we didn't
1630 yet know what resource was associated with this menu.)
1633 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1635 @"no default value for resource \"%@\" in menu item \"%@\"",
1645 set_menu_item_object (def_item, def_obj);
1646 # else // !USE_IPHONE
1647 for (NSMutableArray *a in items) {
1648 // Make sure each array contains the resource key.
1649 [a replaceObjectAtIndex:1 withObject:menu_key];
1650 // Make sure the default item contains the default resource value.
1651 if (def_obj && def_item &&
1652 [def_item isEqualToString:[a objectAtIndex:0]])
1653 [a replaceObjectAtIndex:2 withObject:def_obj];
1655 # endif // !USE_IPHONE
1659 # ifdef USE_PICKER_VIEW
1660 /* Finish tweaking the menu button itself.
1663 [popup setTitle:[def_item title]];
1664 NSRect r = [popup frame];
1665 r.size.width = max_width;
1667 # endif // USE_PICKER_VIEW
1670 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1671 [self placeChild:popup on:parent];
1672 [self bindResource:popup key:menu_key];
1677 # ifdef USE_PICKER_VIEW
1678 // Store the items for this picker in the picker_values array.
1679 // This is so fucking stupid.
1681 int menu_number = [pref_keys count] - 1;
1682 if (! picker_values)
1683 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1684 while ([picker_values count] <= menu_number)
1685 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1686 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1687 [popup reloadAllComponents];
1689 # else // !USE_PICKER_VIEW
1691 [self placeSeparator];
1694 for (NSArray *item in items) {
1695 RadioButton *b = [[RadioButton alloc] initWithIndex:i
1697 [b setLineBreakMode:NSLineBreakByTruncatingHead];
1698 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1699 [self placeChild:b on:parent];
1703 [self placeSeparator];
1705 # endif // !USE_PICKER_VIEW
1706 # endif // !USE_IPHONE
1711 /* Creates an uneditable, wrapping NSTextField to display the given
1712 text enclosed by <description> ... </description> in the XML.
1714 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1716 NSString *text = nil;
1717 NSArray *children = [node children];
1718 int i, count = [children count];
1720 for (i = 0; i < count; i++) {
1721 NSXMLNode *child = [children objectAtIndex:i];
1722 NSString *s = [child objectValue];
1724 text = [text stringByAppendingString:s];
1729 text = unwrap (text);
1731 NSRect rect = [parent frame];
1732 rect.origin.x = rect.origin.y = 0;
1733 rect.size.width = 200;
1734 rect.size.height = 50; // sized later
1736 NSText *lab = [[NSText alloc] initWithFrame:rect];
1737 [lab setEditable:NO];
1738 [lab setDrawsBackground:NO];
1739 [lab setHorizontallyResizable:YES];
1740 [lab setVerticallyResizable:YES];
1741 [lab setString:text];
1746 # else // USE_IPHONE
1748 # ifndef USE_HTML_LABELS
1750 UILabel *lab = [self makeLabel:text];
1751 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1754 # else // USE_HTML_LABELS
1755 HTMLLabel *lab = [[HTMLLabel alloc]
1757 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1758 [lab setFrame:rect];
1760 # endif // USE_HTML_LABELS
1762 [self placeSeparator];
1764 # endif // USE_IPHONE
1766 [self placeChild:lab on:parent];
1771 /* Creates the NSTextField described by the given XML node.
1773 - (void) makeTextField: (NSXMLNode *)node
1774 on: (NSView *)parent
1775 withLabel: (BOOL) label_p
1776 horizontal: (BOOL) horiz_p
1778 NSMutableDictionary *dict = [@{ @"id": @"",
1782 [self parseAttrs:dict node:node];
1783 NSString *label = [dict objectForKey:@"_label"];
1784 NSString *arg = [dict objectForKey:@"arg"];
1786 if (!label && label_p) {
1787 NSAssert1 (0, @"no _label in %@", [node name]);
1791 NSAssert1 (arg, @"no arg in %@", label);
1794 rect.origin.x = rect.origin.y = 0;
1795 rect.size.width = rect.size.height = 10;
1797 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1801 // make the default size be around 30 columns; a typical value for
1802 // these text fields is "xscreensaver-text --cols 40".
1804 [txt setStringValue:@"123456789 123456789 123456789 "];
1806 [[txt cell] setWraps:NO];
1807 [[txt cell] setScrollable:YES];
1808 [txt setStringValue:@""];
1810 # else // USE_IPHONE
1812 txt.adjustsFontSizeToFitWidth = YES;
1813 txt.textColor = [UIColor blackColor];
1814 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1815 txt.placeholder = @"";
1816 txt.borderStyle = UITextBorderStyleRoundedRect;
1817 txt.textAlignment = NSTextAlignmentRight;
1818 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1819 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1820 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1821 txt.clearButtonMode = UITextFieldViewModeAlways;
1822 txt.returnKeyType = UIReturnKeyDone;
1823 txt.delegate = self;
1825 [txt setEnabled: YES];
1827 rect.size.height = [txt.font lineHeight] * 1.2;
1828 [txt setFrame:rect];
1830 # endif // USE_IPHONE
1833 LABEL *lab = [self makeLabel:label];
1834 [self placeChild:lab on:parent];
1838 [self placeChild:txt on:parent right:(label ? YES : NO)];
1840 [self bindSwitch:txt cmdline:arg];
1845 /* Creates the NSTextField described by the given XML node,
1846 and hooks it up to a Choose button and a file selector widget.
1848 - (void) makeFileSelector: (NSXMLNode *)node
1849 on: (NSView *)parent
1850 dirsOnly: (BOOL) dirsOnly
1851 withLabel: (BOOL) label_p
1852 editable: (BOOL) editable_p
1854 # ifndef USE_IPHONE // No files. No selectors.
1855 NSMutableDictionary *dict = [@{ @"id": @"",
1859 [self parseAttrs:dict node:node];
1860 NSString *label = [dict objectForKey:@"_label"];
1861 NSString *arg = [dict objectForKey:@"arg"];
1863 if (!label && label_p) {
1864 NSAssert1 (0, @"no _label in %@", [node name]);
1868 NSAssert1 (arg, @"no arg in %@", label);
1871 rect.origin.x = rect.origin.y = 0;
1872 rect.size.width = rect.size.height = 10;
1874 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1876 // make the default size be around 20 columns.
1878 [txt setStringValue:@"123456789 123456789 "];
1880 [txt setSelectable:YES];
1881 [txt setEditable:editable_p];
1882 [txt setBezeled:editable_p];
1883 [txt setDrawsBackground:editable_p];
1884 [[txt cell] setWraps:NO];
1885 [[txt cell] setScrollable:YES];
1886 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1887 [txt setStringValue:@""];
1891 lab = [self makeLabel:label];
1892 [self placeChild:lab on:parent];
1896 [self placeChild:txt on:parent right:(label ? YES : NO)];
1898 [self bindSwitch:txt cmdline:arg];
1901 // Make the text field and label be the same height, whichever is taller.
1904 rect.size.height = ([lab frame].size.height > [txt frame].size.height
1905 ? [lab frame].size.height
1906 : [txt frame].size.height);
1907 [txt setFrame:rect];
1910 // Now put a "Choose" button next to it.
1912 rect.origin.x = rect.origin.y = 0;
1913 rect.size.width = rect.size.height = 10;
1914 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
1915 [choose setTitle:@"Choose..."];
1916 [choose setBezelStyle:NSRoundedBezelStyle];
1919 [self placeChild:choose on:parent right:YES];
1921 // center the Choose button around the midpoint of the text field.
1922 rect = [choose frame];
1923 rect.origin.y = ([txt frame].origin.y +
1924 (([txt frame].size.height - rect.size.height) / 2));
1925 [choose setFrameOrigin:rect.origin];
1927 [choose setTarget:[parent window]];
1929 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
1931 [choose setAction:@selector(fileSelectorChooseAction:)];
1934 # endif // !USE_IPHONE
1940 /* Runs a modal file selector and sets the text field's value to the
1941 selected file or directory.
1944 do_file_selector (NSTextField *txt, BOOL dirs_p)
1946 NSOpenPanel *panel = [NSOpenPanel openPanel];
1947 [panel setAllowsMultipleSelection:NO];
1948 [panel setCanChooseFiles:!dirs_p];
1949 [panel setCanChooseDirectories:dirs_p];
1951 NSString *file = [txt stringValue];
1952 if ([file length] <= 0) {
1953 file = NSHomeDirectory();
1955 file = [file stringByAppendingPathComponent:@"Pictures"];
1958 // NSString *dir = [file stringByDeletingLastPathComponent];
1960 int result = [panel runModalForDirectory:file //dir
1961 file:nil //[file lastPathComponent]
1963 if (result == NSOKButton) {
1964 NSArray *files = [panel filenames];
1965 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
1966 file = [file stringByAbbreviatingWithTildeInPath];
1967 [txt setStringValue:file];
1969 // Fuck me! Just setting the value of the NSTextField does not cause
1970 // that to end up in the preferences!
1972 NSDictionary *dict = [txt infoForBinding:@"value"];
1973 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
1974 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
1975 if ([path hasPrefix:@"values."]) // WTF.
1976 path = [path substringFromIndex:7];
1977 [[prefs values] setValue:file forKey:path];
1980 // make sure the end of the string is visible.
1981 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
1983 range.location = [file length]-3;
1985 if (! [[txt window] makeFirstResponder:[txt window]])
1986 [[txt window] endEditingFor:nil];
1987 // [[txt window] makeFirstResponder:nil];
1988 [fe setSelectedRange:range];
1989 // [tv scrollRangeToVisible:range];
1990 // [txt setNeedsDisplay:YES];
1991 // [[txt cell] setNeedsDisplay:YES];
1992 // [txt selectAll:txt];
1998 /* Returns the NSTextField that is to the left of or above the NSButton.
2000 static NSTextField *
2001 find_text_field_of_button (NSButton *button)
2003 NSView *parent = [button superview];
2004 NSArray *kids = [parent subviews];
2005 int nkids = [kids count];
2008 for (i = 0; i < nkids; i++) {
2009 NSObject *kid = [kids objectAtIndex:i];
2010 if ([kid isKindOfClass:[NSTextField class]]) {
2011 f = (NSTextField *) kid;
2012 } else if (kid == button) {
2021 - (void) fileSelectorChooseAction:(NSObject *)arg
2023 NSButton *choose = (NSButton *) arg;
2024 NSTextField *txt = find_text_field_of_button (choose);
2025 do_file_selector (txt, NO);
2028 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2030 NSButton *choose = (NSButton *) arg;
2031 NSTextField *txt = find_text_field_of_button (choose);
2032 do_file_selector (txt, YES);
2035 #endif // !USE_IPHONE
2038 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2043 (x) Computer name and time
2044 ( ) Text [__________________________]
2045 ( ) Text file [_________________] [Choose]
2046 ( ) URL [__________________________]
2047 ( ) Shell Cmd [__________________________]
2049 textMode -text-mode date
2050 textMode -text-mode literal textLiteral -text-literal %
2051 textMode -text-mode file textFile -text-file %
2052 textMode -text-mode url textURL -text-url %
2053 textMode -text-mode program textProgram -text-program %
2056 rect.size.width = rect.size.height = 1;
2057 rect.origin.x = rect.origin.y = 0;
2058 NSView *group = [[NSView alloc] initWithFrame:rect];
2059 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2061 Bool program_p = TRUE;
2066 // This is how you link radio buttons together.
2068 NSButtonCell *proto = [[NSButtonCell alloc] init];
2069 [proto setButtonType:NSRadioButton];
2071 rect.origin.x = rect.origin.y = 0;
2072 rect.size.width = rect.size.height = 10;
2073 NSMatrix *matrix = [[NSMatrix alloc]
2075 mode:NSRadioModeMatrix
2077 numberOfRows: 4 + (program_p ? 1 : 0)
2079 [matrix setAllowsEmptySelection:NO];
2081 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
2082 [cnames addObject:@"Computer name and time"];
2083 [cnames addObject:@"Text"];
2084 [cnames addObject:@"File"];
2085 [cnames addObject:@"URL"];
2086 if (program_p) [cnames addObject:@"Shell Cmd"];
2087 [matrix bind:@"content"
2089 withKeyPath:@"arrangedObjects"
2093 [self bindSwitch:matrix cmdline:@"-text-mode %"];
2095 [self placeChild:matrix on:group];
2096 [self placeChild:rgroup on:group right:YES];
2100 # else // USE_IPHONE
2102 NSView *rgroup = parent;
2105 // <select id="textMode">
2106 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
2107 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
2108 // <option id="url" _label="Display URL"/>
2111 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2112 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2114 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2115 [node3 setAttributesAsDictionary:
2117 @"arg-set": @"-text-mode date",
2118 @"_label": @"Display the date and time" }];
2119 [node3 setParent: node2];
2122 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2123 [node3 setAttributesAsDictionary:
2125 @"arg-set": @"-text-mode literal",
2126 @"_label": @"Display static text" }];
2127 [node3 setParent: node2];
2130 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2131 [node3 setAttributesAsDictionary:
2133 @"_label": @"Display the contents of a URL" }];
2134 [node3 setParent: node2];
2137 [self makeOptionMenu:node2 on:rgroup];
2139 # endif // USE_IPHONE
2142 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2143 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2144 [node2 setAttributesAsDictionary:
2145 @{ @"id": @"textLiteral",
2146 @"arg": @"-text-literal %",
2148 @"_label": @"Text to display"
2151 [self makeTextField:node2 on:rgroup
2159 // rect = [last_child(rgroup) frame];
2161 /* // trying to make the text fields be enabled only when the checkbox is on..
2162 control = last_child (rgroup);
2163 [control bind:@"enabled"
2164 toObject:[matrix cellAtRow:1 column:0]
2165 withKeyPath:@"value"
2171 // <file id="textFile" _label="" arg-set="-text-file %"/>
2172 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2173 [node2 setAttributesAsDictionary:
2174 @{ @"id": @"textFile",
2175 @"arg": @"-text-file %" }];
2176 [self makeFileSelector:node2 on:rgroup
2177 dirsOnly:NO withLabel:NO editable:NO];
2178 # endif // !USE_IPHONE
2180 // rect = [last_child(rgroup) frame];
2182 // <string id="textURL" _label="" arg-set="text-url %"/>
2183 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2184 [node2 setAttributesAsDictionary:
2185 @{ @"id": @"textURL",
2186 @"arg": @"-text-url %",
2188 @"_label": @"URL to display",
2191 [self makeTextField:node2 on:rgroup
2199 // rect = [last_child(rgroup) frame];
2203 // <string id="textProgram" _label="" arg-set="text-program %"/>
2204 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2205 [node2 setAttributesAsDictionary:
2206 @{ @"id": @"textProgram",
2207 @"arg": @"-text-program %",
2209 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2212 // rect = [last_child(rgroup) frame];
2214 layout_group (rgroup, NO);
2216 rect = [rgroup frame];
2217 rect.size.width += 35; // WTF? Why is rgroup too narrow?
2218 [rgroup setFrame:rect];
2221 // Set the height of the cells in the radio-box matrix to the height of
2222 // the (last of the) text fields.
2223 control = last_child (rgroup);
2224 rect = [control frame];
2225 rect.size.width = 30; // width of the string "Text", plus a bit...
2227 rect.size.width += 25;
2228 rect.size.height += LINE_SPACING;
2229 [matrix setCellSize:rect.size];
2230 [matrix sizeToCells];
2232 layout_group (group, YES);
2233 rect = [matrix frame];
2234 rect.origin.x += rect.size.width + COLUMN_SPACING;
2235 rect.origin.y -= [control frame].size.height - LINE_SPACING;
2236 [rgroup setFrameOrigin:rect.origin];
2238 // now cheat on the size of the matrix: allow it to overlap (underlap)
2241 rect.size = [matrix cellSize];
2242 rect.size.width = 300;
2243 [matrix setCellSize:rect.size];
2244 [matrix sizeToCells];
2246 // Cheat on the position of the stuff on the right (the rgroup).
2247 // GAAAH, this code is such crap!
2248 rect = [rgroup frame];
2250 [rgroup setFrame:rect];
2253 rect.size.width = rect.size.height = 0;
2254 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2255 [box setTitlePosition:NSAtTop];
2256 [box setBorderType:NSBezelBorder];
2257 [box setTitle:@"Display Text"];
2259 rect.size.width = rect.size.height = 12;
2260 [box setContentViewMargins:rect.size];
2261 [box setContentView:group];
2264 [self placeChild:box on:parent];
2266 # endif // !USE_IPHONE
2270 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2273 [x] Grab desktop images
2274 [ ] Choose random image:
2275 [__________________________] [Choose]
2277 <boolean id="grabDesktopImages" _label="Grab desktop images"
2278 arg-unset="-no-grab-desktop"/>
2279 <boolean id="chooseRandomImages" _label="Grab desktop images"
2280 arg-unset="-choose-random-images"/>
2281 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2284 NSXMLElement *node2;
2287 # define SCREENS "Grab desktop images"
2288 # define PHOTOS "Choose random images"
2290 # define SCREENS "Grab screenshots"
2291 # define PHOTOS "Use photo library"
2294 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2295 [node2 setAttributesAsDictionary:
2296 @{ @"id": @"grabDesktopImages",
2297 @"_label": @ SCREENS,
2298 @"arg-unset": @"-no-grab-desktop",
2300 [self makeCheckbox:node2 on:parent];
2302 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2303 [node2 setAttributesAsDictionary:
2304 @{ @"id": @"chooseRandomImages",
2305 @"_label": @ PHOTOS,
2306 @"arg-set": @"-choose-random-images",
2308 [self makeCheckbox:node2 on:parent];
2310 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2311 [node2 setAttributesAsDictionary:
2312 @{ @"id": @"imageDirectory",
2313 @"_label": @"Images from:",
2314 @"arg": @"-image-directory %",
2316 [self makeFileSelector:node2 on:parent
2317 dirsOnly:YES withLabel:YES editable:YES];
2323 // Add a second, explanatory label below the file/URL selector.
2326 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2327 [self placeChild:lab2 on:parent];
2329 // Pack it in a little tighter vertically.
2330 NSRect r2 = [lab2 frame];
2333 [lab2 setFrameOrigin:r2.origin];
2335 # endif // USE_IPHONE
2339 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2343 [x] Check for Updates [ Monthly ]
2346 <boolean id="automaticallyChecksForUpdates"
2347 _label="Automatically check for updates"
2348 arg-unset="-no-automaticallyChecksForUpdates" />
2349 <select id="updateCheckInterval">
2350 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2351 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/>
2352 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2353 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2361 rect.size.width = rect.size.height = 1;
2362 rect.origin.x = rect.origin.y = 0;
2363 NSView *group = [[NSView alloc] initWithFrame:rect];
2365 NSXMLElement *node2;
2369 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2370 [node2 setAttributesAsDictionary:
2371 @{ @"id": @SUSUEnableAutomaticChecksKey,
2372 @"_label": @"Automatically check for updates",
2373 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2375 [self makeCheckbox:node2 on:group];
2379 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2380 [node2 setAttributesAsDictionary:
2381 @{ @"id": @SUScheduledCheckIntervalKey }];
2385 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2386 [node3 setAttributesAsDictionary:
2387 @{ @"id": @"hourly",
2388 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2389 @"_label": @"Hourly" }];
2390 [node3 setParent: node2];
2393 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2394 [node3 setAttributesAsDictionary:
2396 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2397 @"_label": @"Daily" }];
2398 [node3 setParent: node2];
2401 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2402 [node3 setAttributesAsDictionary:
2403 @{ @"id": @"weekly",
2404 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2405 @"_label": @"Weekly",
2407 [node3 setParent: node2];
2410 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2411 [node3 setAttributesAsDictionary:
2412 @{ @"id": @"monthly",
2413 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2414 @"_label": @"Monthly",
2416 [node3 setParent: node2];
2420 [self makeOptionMenu:node2 on:group];
2423 layout_group (group, TRUE);
2425 rect.size.width = rect.size.height = 0;
2426 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2427 [box setTitlePosition:NSNoTitle];
2428 [box setBorderType:NSNoBorder];
2429 [box setContentViewMargins:rect.size];
2430 [box setContentView:group];
2433 [self placeChild:box on:parent];
2435 # endif // !USE_IPHONE
2439 #pragma mark Layout for controls
2444 last_child (NSView *parent)
2446 NSArray *kids = [parent subviews];
2447 int nkids = [kids count];
2451 return [kids objectAtIndex:nkids-1];
2453 #endif // USE_IPHONE
2456 /* Add the child as a subview of the parent, positioning it immediately
2457 below or to the right of the previously-added child of that view.
2459 - (void) placeChild:
2465 on:(NSView *)parent right:(BOOL)right_p
2468 NSRect rect = [child frame];
2469 NSView *last = last_child (parent);
2471 rect.origin.x = LEFT_MARGIN;
2472 rect.origin.y = ([parent frame].size.height - rect.size.height
2474 } else if (right_p) {
2475 rect = [last frame];
2476 rect.origin.x += rect.size.width + COLUMN_SPACING;
2478 rect = [last frame];
2479 rect.origin.x = LEFT_MARGIN;
2480 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2482 NSRect r = [child frame];
2483 r.origin = rect.origin;
2485 [parent addSubview:child];
2487 # else // USE_IPHONE
2489 // Controls is an array of arrays of the controls, divided into sections.
2491 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2492 if ([controls count] == 0)
2493 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2494 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2496 if (!right_p || [current count] == 0) {
2497 // Nothing on the current line. Add this object.
2498 [current addObject: child];
2500 // Something's on the current line already.
2501 NSObject *old = [current objectAtIndex:[current count]-1];
2502 if ([old isKindOfClass:[NSMutableArray class]]) {
2503 // Already an array in this cell. Append.
2504 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2505 [(NSMutableArray *) old addObject: child];
2507 // Replace the control in this cell with an array, then app
2508 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2509 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2512 # endif // USE_IPHONE
2516 - (void) placeChild:(NSView *)child on:(NSView *)parent
2518 [self placeChild:child on:parent right:NO];
2524 // Start putting subsequent children in a new group, to create a new
2525 // section on the UITableView.
2527 - (void) placeSeparator
2529 if (! controls) return;
2530 if ([controls count] == 0) return;
2531 if ([[controls objectAtIndex:[controls count]-1]
2533 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2535 #endif // USE_IPHONE
2539 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2540 wrapped in <hgroup> or <vgroup> in the XML.
2542 - (void) makeGroup:(NSXMLNode *)node
2544 horizontal:(BOOL) horiz_p
2547 if (!horiz_p) [self placeSeparator];
2548 [self traverseChildren:node on:parent];
2549 if (!horiz_p) [self placeSeparator];
2550 # else // !USE_IPHONE
2552 rect.size.width = rect.size.height = 1;
2553 rect.origin.x = rect.origin.y = 0;
2554 NSView *group = [[NSView alloc] initWithFrame:rect];
2555 [self traverseChildren:node on:group];
2557 layout_group (group, horiz_p);
2559 rect.size.width = rect.size.height = 0;
2560 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2561 [box setTitlePosition:NSNoTitle];
2562 [box setBorderType:NSNoBorder];
2563 [box setContentViewMargins:rect.size];
2564 [box setContentView:group];
2567 [self placeChild:box on:parent];
2568 # endif // !USE_IPHONE
2574 layout_group (NSView *group, BOOL horiz_p)
2576 NSArray *kids = [group subviews];
2577 int nkids = [kids count];
2579 double maxx = 0, miny = 0;
2580 for (i = 0; i < nkids; i++) {
2581 NSView *kid = [kids objectAtIndex:i];
2582 NSRect r = [kid frame];
2585 maxx += r.size.width + COLUMN_SPACING;
2586 if (r.size.height > -miny) miny = -r.size.height;
2588 if (r.size.width > maxx) maxx = r.size.width;
2589 miny = r.origin.y - r.size.height;
2596 rect.size.width = maxx;
2597 rect.size.height = -miny;
2598 [group setFrame:rect];
2601 for (i = 0; i < nkids; i++) {
2602 NSView *kid = [kids objectAtIndex:i];
2603 NSRect r = [kid frame];
2605 r.origin.y = rect.size.height - r.size.height;
2607 x += r.size.width + COLUMN_SPACING;
2614 #endif // !USE_IPHONE
2617 /* Create some kind of control corresponding to the given XML node.
2619 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2621 NSString *name = [node name];
2623 if ([node kind] == NSXMLCommentKind)
2626 if ([node kind] == NSXMLTextKind) {
2627 NSString *s = [(NSString *) [node objectValue]
2628 stringByTrimmingCharactersInSet:
2629 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2630 if (! [s isEqualToString:@""]) {
2631 NSAssert1 (0, @"unexpected text: %@", s);
2636 if ([node kind] != NSXMLElementKind) {
2637 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2641 if ([name isEqualToString:@"hgroup"] ||
2642 [name isEqualToString:@"vgroup"]) {
2644 [self makeGroup:node on:parent
2645 horizontal:[name isEqualToString:@"hgroup"]];
2647 } else if ([name isEqualToString:@"command"]) {
2648 // do nothing: this is the "-root" business
2650 } else if ([name isEqualToString:@"boolean"]) {
2651 [self makeCheckbox:node on:parent];
2653 } else if ([name isEqualToString:@"string"]) {
2654 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2656 } else if ([name isEqualToString:@"file"]) {
2657 [self makeFileSelector:node on:parent
2658 dirsOnly:NO withLabel:YES editable:NO];
2660 } else if ([name isEqualToString:@"number"]) {
2661 [self makeNumberSelector:node on:parent];
2663 } else if ([name isEqualToString:@"select"]) {
2664 [self makeOptionMenu:node on:parent];
2666 } else if ([name isEqualToString:@"_description"]) {
2667 [self makeDescLabel:node on:parent];
2669 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2670 [self makeTextLoaderControlBox:node on:parent];
2672 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2673 [self makeImageLoaderControlBox:node on:parent];
2675 } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2676 [self makeUpdaterControlBox:node on:parent];
2679 NSAssert1 (0, @"unknown tag: %@", name);
2684 /* Iterate over and process the children of this XML node.
2686 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2688 NSArray *children = [node children];
2689 int i, count = [children count];
2690 for (i = 0; i < count; i++) {
2691 NSXMLNode *child = [children objectAtIndex:i];
2692 [self makeControl:child on:parent];
2699 /* Kludgey magic to make the window enclose the controls we created.
2702 fix_contentview_size (NSView *parent)
2705 NSArray *kids = [parent subviews];
2706 int nkids = [kids count];
2707 NSView *text = 0; // the NSText at the bottom of the window
2708 double maxx = 0, miny = 0;
2711 /* Find the size of the rectangle taken up by each of the children
2712 except the final "NSText" child.
2714 for (i = 0; i < nkids; i++) {
2715 NSView *kid = [kids objectAtIndex:i];
2716 if ([kid isKindOfClass:[NSText class]]) {
2721 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2722 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2723 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2724 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2725 // f.origin.y + f.size.height, [kid class]);
2728 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2730 /* Now that we know the width of the window, set the width of the NSText to
2731 that, so that it can decide what its height needs to be.
2733 if (! text) abort();
2735 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2736 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2737 // f.origin.y + f.size.height, [text class]);
2739 // set the NSText's width (this changes its height).
2740 f.size.width = maxx - LEFT_MARGIN;
2743 // position the NSText below the last child (this gives us a new miny).
2745 f.origin.y = miny - f.size.height - LINE_SPACING;
2746 miny = f.origin.y - LINE_SPACING;
2749 // Lock the width of the field and unlock the height, and let it resize
2750 // once more, to compute the proper height of the text for that width.
2752 [(NSText *) text setHorizontallyResizable:NO];
2753 [(NSText *) text setVerticallyResizable:YES];
2754 [(NSText *) text sizeToFit];
2756 // Now lock the height too: no more resizing this text field.
2758 [(NSText *) text setVerticallyResizable:NO];
2760 // Now reposition the top edge of the text field to be back where it
2761 // was before we changed the height.
2763 float oh = f.size.height;
2765 float dh = f.size.height - oh;
2768 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2769 // If we do this in 10.6, the text field moves down, off the window.
2770 // So instead we repair it at the end, at the "WTF2" comment.
2773 // Also adjust the parent height by the change in height of the text field.
2776 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2777 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2778 // f.origin.y + f.size.height, [text class]);
2781 /* Set the contentView to the size of the children.
2784 // float yoff = f.size.height;
2785 f.size.width = maxx + LEFT_MARGIN;
2786 f.size.height = -(miny - LEFT_MARGIN*2);
2787 // yoff = f.size.height - yoff;
2788 [parent setFrame:f];
2790 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2791 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2793 /* Now move all of the kids up into the window.
2796 float shift = f.size.height;
2797 // NSLog(@"shift: %3.0f", shift);
2798 for (i = 0; i < nkids; i++) {
2799 NSView *kid = [kids objectAtIndex:i];
2801 f.origin.y += shift;
2803 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2804 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2805 // f.origin.y + f.size.height, [kid class]);
2810 parent: 420 x 541 @ 0 0
2811 text: 380 x 100 @ 20 22 miny=-501
2814 parent: 420 x 541 @ 0 0
2815 text: 380 x 100 @ 20 50 miny=-501
2818 // #### WTF2: See "WTF" above. If the text field is off the screen,
2819 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2822 if (f.origin.y < 50) { // magic numbers, yay
2827 /* Set the kids to track the top left corner of the window when resized.
2828 Set the NSText to track the bottom right corner as well.
2830 for (i = 0; i < nkids; i++) {
2831 NSView *kid = [kids objectAtIndex:i];
2832 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2833 if ([kid isKindOfClass:[NSText class]])
2834 mask |= NSViewWidthSizable|NSViewHeightSizable;
2835 [kid setAutoresizingMask:mask];
2838 # endif // !USE_IPHONE
2844 wrap_with_buttons (NSWindow *window, NSView *panel)
2848 // Make a box to hold the buttons at the bottom of the window.
2850 rect = [panel frame];
2851 rect.origin.x = rect.origin.y = 0;
2852 rect.size.height = 10;
2853 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2854 [bbox setTitlePosition:NSNoTitle];
2855 [bbox setBorderType:NSNoBorder];
2857 // Make some buttons: Default, Cancel, OK
2859 rect.origin.x = rect.origin.y = 0;
2860 rect.size.width = rect.size.height = 10;
2861 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2862 [reset setTitle:@"Reset to Defaults"];
2863 [reset setBezelStyle:NSRoundedBezelStyle];
2866 rect = [reset frame];
2867 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2868 [ok setTitle:@"OK"];
2869 [ok setBezelStyle:NSRoundedBezelStyle];
2871 rect = [bbox frame];
2872 rect.origin.x = rect.size.width - [ok frame].size.width;
2873 [ok setFrameOrigin:rect.origin];
2876 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2877 [cancel setTitle:@"Cancel"];
2878 [cancel setBezelStyle:NSRoundedBezelStyle];
2880 rect.origin.x -= [cancel frame].size.width + 10;
2881 [cancel setFrameOrigin:rect.origin];
2883 // Bind OK to RET and Cancel to ESC.
2884 [ok setKeyEquivalent:@"\r"];
2885 [cancel setKeyEquivalent:@"\e"];
2887 // The correct width for OK and Cancel buttons is 68 pixels
2888 // ("Human Interface Guidelines: Controls: Buttons:
2889 // Push Button Specifications").
2892 rect.size.width = 68;
2895 rect = [cancel frame];
2896 rect.size.width = 68;
2897 [cancel setFrame:rect];
2899 // It puts the buttons in the box or else it gets the hose again
2901 [bbox addSubview:ok];
2902 [bbox addSubview:cancel];
2903 [bbox addSubview:reset];
2906 // make a box to hold the button-box, and the preferences view
2908 rect = [bbox frame];
2909 rect.origin.y += rect.size.height;
2910 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
2911 [pbox setTitlePosition:NSNoTitle];
2912 [pbox setBorderType:NSBezelBorder];
2914 // Enforce a max height on the dialog, so that it's obvious to me
2915 // (on a big screen) when the dialog will fall off the bottom of
2916 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
2918 NSRect f = [panel frame];
2919 int screen_height = (768 // shortest "modern" Mac display
2921 - 56 // System Preferences toolbar
2922 - 140 // default magnified bottom dock icon
2924 if (f.size.height > screen_height) {
2925 NSLog(@"%@ height was %.0f; clipping to %d",
2926 [panel class], f.size.height, screen_height);
2927 f.size.height = screen_height;
2932 [pbox addSubview:panel];
2933 [pbox addSubview:bbox];
2936 [reset setAutoresizingMask:NSViewMaxXMargin];
2937 [cancel setAutoresizingMask:NSViewMinXMargin];
2938 [ok setAutoresizingMask:NSViewMinXMargin];
2939 [bbox setAutoresizingMask:NSViewWidthSizable];
2943 [ok setTarget:window];
2944 [cancel setTarget:window];
2945 [reset setTarget:window];
2946 [ok setAction:@selector(okAction:)];
2947 [cancel setAction:@selector(cancelAction:)];
2948 [reset setAction:@selector(resetAction:)];
2952 #endif // !USE_IPHONE
2955 /* Iterate over and process the children of the root node of the XML document.
2957 - (void)traverseTree
2960 NSView *parent = [self view];
2962 NSWindow *parent = self;
2964 NSXMLNode *node = xml_root;
2966 if (![[node name] isEqualToString:@"screensaver"]) {
2967 NSAssert (0, @"top level node is not <xscreensaver>");
2970 saver_name = [self parseXScreenSaverTag: node];
2971 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
2973 [saver_name retain];
2978 rect.origin.x = rect.origin.y = 0;
2979 rect.size.width = rect.size.height = 1;
2981 NSView *panel = [[NSView alloc] initWithFrame:rect];
2982 [self traverseChildren:node on:panel];
2983 fix_contentview_size (panel);
2985 NSView *root = wrap_with_buttons (parent, panel);
2986 [userDefaultsController setAppliesImmediately:NO];
2987 [globalDefaultsController setAppliesImmediately:NO];
2989 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
2991 rect = [parent frameRectForContentRect:[root frame]];
2992 [parent setFrame:rect display:NO];
2993 [parent setMinSize:rect.size];
2995 [parent setContentView:root];
2997 # else // USE_IPHONE
2999 CGRect r = [parent frame];
3000 r.size = [[UIScreen mainScreen] bounds].size;
3001 [parent setFrame:r];
3002 [self traverseChildren:node on:parent];
3004 # endif // USE_IPHONE
3008 - (void)parser:(NSXMLParser *)parser
3009 didStartElement:(NSString *)elt
3010 namespaceURI:(NSString *)ns
3011 qualifiedName:(NSString *)qn
3012 attributes:(NSDictionary *)attrs
3014 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3015 [e setKind:SimpleXMLElementKind];
3016 [e setAttributesAsDictionary:attrs];
3017 NSXMLElement *p = xml_parsing;
3021 xml_root = xml_parsing;
3024 - (void)parser:(NSXMLParser *)parser
3025 didEndElement:(NSString *)elt
3026 namespaceURI:(NSString *)ns
3027 qualifiedName:(NSString *)qn
3029 NSXMLElement *p = xml_parsing;
3031 NSLog(@"extra close: %@", elt);
3032 } else if (![[p name] isEqualToString:elt]) {
3033 NSLog(@"%@ closed by %@", [p name], elt);
3035 NSXMLElement *n = xml_parsing;
3036 xml_parsing = [n parent];
3041 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3043 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3044 [e setKind:SimpleXMLTextKind];
3045 NSXMLElement *p = xml_parsing;
3047 [e setObjectValue: string];
3052 # ifdef USE_PICKER_VIEW
3054 #pragma mark UIPickerView delegate methods
3056 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3058 return 1; // Columns
3061 - (NSInteger)pickerView:(UIPickerView *)pv
3062 numberOfRowsInComponent:(NSInteger)column
3064 NSAssert (column == 0, @"weird column");
3065 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3066 if (! a) return 0; // Too early?
3070 - (CGFloat)pickerView:(UIPickerView *)pv
3071 rowHeightForComponent:(NSInteger)column
3076 - (CGFloat)pickerView:(UIPickerView *)pv
3077 widthForComponent:(NSInteger)column
3079 NSAssert (column == 0, @"weird column");
3080 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3081 if (! a) return 0; // Too early?
3083 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3085 for (NSArray *a2 in a) {
3086 NSString *s = [a2 objectAtIndex:0];
3087 CGSize r = [s sizeWithFont:f];
3088 if (r.width > max) max = r.width;
3091 max *= 1.7; // WTF!!
3103 - (NSString *)pickerView:(UIPickerView *)pv
3104 titleForRow:(NSInteger)row
3105 forComponent:(NSInteger)column
3107 NSAssert (column == 0, @"weird column");
3108 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3109 if (! a) return 0; // Too early?
3110 a = [a objectAtIndex:row];
3111 NSAssert (a, @"internal error");
3112 return [a objectAtIndex:0];
3115 # endif // USE_PICKER_VIEW
3118 #pragma mark UITableView delegate methods
3120 - (void) addResetButton
3122 [[self navigationItem]
3123 setRightBarButtonItem: [[UIBarButtonItem alloc]
3124 initWithTitle: @"Reset to Defaults"
3125 style: UIBarButtonItemStyleBordered
3127 action:@selector(resetAction:)]];
3128 NSString *s = saver_name;
3129 if ([self view].frame.size.width > 320)
3130 s = [s stringByAppendingString: @" Settings"];
3131 [self navigationItem].title = s;
3135 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3140 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3141 // Number of vertically-stacked white boxes.
3142 return [controls count];
3145 - (NSInteger)tableView:(UITableView *)tableView
3146 numberOfRowsInSection:(NSInteger)section
3148 // Number of lines in each vertically-stacked white box.
3149 NSAssert (controls, @"internal error");
3150 return [[controls objectAtIndex:section] count];
3153 - (NSString *)tableView:(UITableView *)tv
3154 titleForHeaderInSection:(NSInteger)section
3156 // Titles above each vertically-stacked white box.
3157 // if (section == 0)
3158 // return [saver_name stringByAppendingString:@" Settings"];
3163 - (CGFloat)tableView:(UITableView *)tv
3164 heightForRowAtIndexPath:(NSIndexPath *)ip
3166 CGFloat h = [tv rowHeight];
3168 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3169 objectAtIndex:[ip indexAtPosition:1]];
3171 if ([ctl isKindOfClass:[NSArray class]]) {
3172 NSArray *set = (NSArray *) ctl;
3173 switch ([set count]) {
3175 # ifdef LABEL_ABOVE_SLIDER
3176 h *= 1.7; break; // label + left/slider/right: 2 1/2 lines
3178 case 3: h *= 1.2; break; // left/slider/right: 1 1/2 lines
3180 if ([[set objectAtIndex:1] isKindOfClass:[UITextField class]])
3184 } else if ([ctl isKindOfClass:[UILabel class]]) {
3185 UILabel *t = (UILabel *) ctl;
3187 r.size.width = 250; // WTF! Black magic!
3188 r.size.width -= LEFT_MARGIN;
3192 h = r.size.height + LINE_SPACING * 3;
3193 # ifdef USE_HTML_LABELS
3195 } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3197 HTMLLabel *t = (HTMLLabel *) ctl;
3199 r.size.width = [tv frame].size.width;
3200 r.size.width -= LEFT_MARGIN * 2;
3204 h = r.size.height + LINE_SPACING * 3;
3206 # endif // USE_HTML_LABELS
3208 CGFloat h2 = [ctl frame].size.height;
3209 h2 += LINE_SPACING * 2;
3217 - (void)refreshTableView
3219 UITableView *tv = (UITableView *) [self view];
3220 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3221 int rows = [self numberOfSectionsInTableView:tv];
3222 for (int i = 0; i < rows; i++) {
3223 int cols = [self tableView:tv numberOfRowsInSection:i];
3224 for (int j = 0; j < cols; j++) {
3228 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3233 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3238 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3240 [NSTimer scheduledTimerWithTimeInterval: 0
3242 selector:@selector(refreshTableView)
3248 #ifndef USE_PICKER_VIEW
3250 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3251 button:(RadioButton *)b
3253 NSArray *item = [[b items] objectAtIndex: [b index]];
3254 NSString *pref_key = [item objectAtIndex:1];
3255 NSObject *pref_val = [item objectAtIndex:2];
3257 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3259 // Convert them both to strings and compare those, so that
3260 // we don't get screwed by int 1 versus string "1".
3261 // Will boolean true/1 screw us here too?
3263 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3264 ? (NSString *) pref_val
3265 : [(NSNumber *) pref_val stringValue]);
3266 NSString *current_str = ([current isKindOfClass:[NSString class]]
3267 ? (NSString *) current
3268 : [(NSNumber *) current stringValue]);
3269 BOOL match_p = [current_str isEqualToString:pref_str];
3271 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3274 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3276 [cell setAccessoryType:UITableViewCellAccessoryNone];
3280 - (void)tableView:(UITableView *)tv
3281 didSelectRowAtIndexPath:(NSIndexPath *)ip
3283 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3284 objectAtIndex:[ip indexAtPosition:1]];
3285 if (! [ctl isKindOfClass:[RadioButton class]])
3288 [self radioAction:ctl];
3289 [self refreshTableView];
3293 #endif // !USE_PICKER_VIEW
3297 - (UITableViewCell *)tableView:(UITableView *)tv
3298 cellForRowAtIndexPath:(NSIndexPath *)ip
3301 /* #### If we re-use cells, then clicking on a checkbox RadioButton
3302 (in non-USE_PICKER_VIEW mode) makes all the cells disappear.
3303 This doesn't happen if we don't re-use any cells. Oh well.
3305 NSString *id = [NSString stringWithFormat: @"%d:%d",
3306 [ip indexAtPosition:0],
3307 [ip indexAtPosition:1]];
3308 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier: id];
3310 if (cell) return cell;
3313 UITableViewCell *cell;
3316 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3317 reuseIdentifier: id]
3319 cell.selectionStyle = UITableViewCellSelectionStyleNone;
3321 CGRect p = [cell frame];
3324 p.size.height = [self tableView:tv heightForRowAtIndexPath:ip];
3327 // Allocate more space to the controls on iPad screens,
3328 // and on landscape-mode iPhones.
3329 CGFloat ww = [tv frame].size.width;
3330 CGFloat left_edge = (ww > 700
3331 ? p.size.width * 0.9
3333 ? p.size.width * 0.5
3334 : p.size.width * 0.3);
3335 CGFloat right_edge = p.origin.x + p.size.width - LEFT_MARGIN;
3338 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3339 objectAtIndex:[ip indexAtPosition:1]];
3341 if ([ctl isKindOfClass:[NSArray class]]) {
3342 // This cell has a set of objects in it.
3343 NSArray *set = (NSArray *) ctl;
3344 switch ([set count]) {
3347 // With 2 elements, the first of the pair must be a label.
3348 UILabel *label = (UILabel *) [set objectAtIndex: 0];
3349 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3350 ctl = [set objectAtIndex: 1];
3353 if ([ctl isKindOfClass:[UISwitch class]]) {
3354 // Flush right checkboxes.
3355 ctl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
3356 r.size.width = 80; // Magic.
3357 r.origin.x = right_edge - r.size.width;
3359 // Expandable sliders.
3360 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
3361 r.origin.x = left_edge;
3362 r.size.width = right_edge - r.origin.x;
3364 r.origin.y = (p.size.height - r.size.height) / 2;
3368 NSView *box = [[UIView alloc] initWithFrame:p];
3369 [box addSubview: ctl];
3371 // cell.textLabel.text = [(UILabel *) ctl text];
3373 r.origin.x = LEFT_MARGIN;
3375 r.size.width = [ctl frame].origin.x - r.origin.x;
3376 r.size.height = p.size.height;
3378 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3379 label.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3380 box. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3381 [box addSubview: label];
3389 // With 3 elements, the first and last must be labels.
3390 // With 4 elements, the first, second and last must be labels.
3392 UILabel *top = ([set count] == 4
3393 ? [set objectAtIndex: i++]
3395 UILabel *left = [set objectAtIndex: i++];
3396 NSView *mid = [set objectAtIndex: i++];
3397 UILabel *right = [set objectAtIndex: i++];
3398 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3399 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3400 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3401 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3403 // 3 elements: control at top of cell.
3404 // 4 elements: center the control vertically.
3406 # ifdef LABEL_ABOVE_SLIDER
3407 left_edge = LEFT_MARGIN;
3409 r.origin.x = left_edge;
3410 r.size.width = right_edge - r.origin.x;
3411 r.origin.y = ([set count] == 3
3413 : (p.size.height - r.size.height) / 2);
3416 // Top label goes above, flush center/top.
3418 r.size = [[top text] sizeWithFont:[top font]
3420 CGSizeMake (p.size.width - LEFT_MARGIN*2,
3422 lineBreakMode:[top lineBreakMode]];
3423 r.origin.x = (p.size.width - r.size.width) / 2;
3428 // Left label goes under control, flush left/bottom.
3429 r.size = [[left text] sizeWithFont:[left font]
3431 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3433 lineBreakMode:[left lineBreakMode]];
3434 r.origin.x = [mid frame].origin.x;
3435 r.origin.y = p.size.height - r.size.height - 4;
3438 // Right label goes under control, flush right/bottom.
3440 r.size = [[right text] sizeWithFont:[right font]
3442 CGSizeMake(p.size.width - LEFT_MARGIN*2,
3444 lineBreakMode:[right lineBreakMode]];
3445 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3447 r.origin.y = [left frame].origin.y;
3451 ctl = [[UIView alloc] initWithFrame:p];
3453 # ifdef LABEL_ABOVE_SLIDER
3454 [ctl addSubview: top];
3455 top.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin|
3456 UIViewAutoresizingFlexibleRightMargin);
3459 r.origin.x = LEFT_MARGIN;
3461 r.size.width = [mid frame].origin.x - r.origin.x;
3462 r.size.height = p.size.height;
3464 top.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3465 [ctl addSubview: top];
3468 [ctl addSubview: left];
3469 [ctl addSubview: mid];
3470 [ctl addSubview: right];
3472 left. autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
3473 mid. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3474 right.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
3475 ctl. autoresizingMask = UIViewAutoresizingFlexibleWidth;
3479 NSAssert (0, @"unhandled size");
3482 // A single view, not a pair.
3485 r.origin.x = LEFT_MARGIN;
3486 r.origin.y = LINE_SPACING;
3487 r.size.width = right_edge - r.origin.x;
3490 ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth;
3492 # ifndef USE_PICKER_VIEW
3493 if ([ctl isKindOfClass:[RadioButton class]])
3494 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3495 # endif // USE_PICKER_VIEW
3498 if ([ctl isKindOfClass:[UILabel class]]) {
3499 // Make label full height to allow text to line-wrap if necessary.
3501 r.origin.y = p.origin.y;
3502 r.size.height = p.size.height;
3506 [cell.contentView addSubview: ctl];
3510 # endif // USE_IPHONE
3513 /* When this object is instantiated, it parses the XML file and creates
3514 controls on itself that are hooked up to the appropriate preferences.
3515 The default size of the view is just big enough to hold them all.
3517 - (id)initWithXML: (NSData *) xml_data
3518 options: (const XrmOptionDescRec *) _opts
3519 controller: (NSUserDefaultsController *) _prefs
3520 globalController: (NSUserDefaultsController *) _globalPrefs
3521 defaults: (NSDictionary *) _defs
3524 self = [super init];
3525 # else // !USE_IPHONE
3526 self = [super initWithStyle:UITableViewStyleGrouped];
3527 self.title = [saver_name stringByAppendingString:@" Settings"];
3528 # endif // !USE_IPHONE
3529 if (! self) return 0;
3531 // instance variables
3533 defaultOptions = _defs;
3534 userDefaultsController = [_prefs retain];
3535 globalDefaultsController = [_globalPrefs retain];
3537 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3540 NSAssert1 (0, @"XML Error: %@",
3541 [[NSString alloc] initWithData:xml_data
3542 encoding:NSUTF8StringEncoding]);
3545 [xmlDoc setDelegate:self];
3546 if (! [xmlDoc parse]) {
3547 NSError *err = [xmlDoc parserError];
3548 NSAssert2 (0, @"XML Error: %@: %@",
3549 [[NSString alloc] initWithData:xml_data
3550 encoding:NSUTF8StringEncoding],
3555 [self traverseTree];
3559 [self addResetButton];
3568 [saver_name release];
3569 [userDefaultsController release];
3570 [globalDefaultsController release];
3573 [pref_keys release];
3574 [pref_ctls release];
3575 # ifdef USE_PICKER_VIEW
3576 [picker_values release];