1 /* xscreensaver, Copyright (c) 2006-2014 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 = UIViewAutoresizingNone; // we do it manually
232 webView.autoresizingMask = UIViewAutoresizingNone;
233 webView.scrollView.scrollEnabled = NO;
234 webView.scrollView.bounces = NO;
236 [webView setBackgroundColor:[UIColor clearColor]];
238 [self addSubview: webView];
243 - (id) initWithText:(NSString *)t font:(UIFont *)f
245 self = [self initWithHTML:@"" font:f];
246 if (! self) return 0;
252 - (void) setHTML: (NSString *)h
256 if (html) [html release];
259 [NSString stringWithFormat:
260 @"<!DOCTYPE HTML PUBLIC "
261 "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
262 " \"http://www.w3.org/TR/html4/loose.dtd\">"
265 // "<META NAME=\"viewport\" CONTENT=\""
266 // "width=device-width"
267 // "initial-scale=1.0;"
268 // "maximum-scale=1.0;\">"
272 " margin: 0; padding: 0; border: 0;"
273 " font-family: \"%@\";"
274 " font-size: %.4fpx;" // Must be "px", not "pt"!
275 " line-height: %.4fpx;" // And no spaces before it.
276 " -webkit-text-size-adjust: none;"
289 [webView stopLoading];
290 [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
294 static char *anchorize (const char *url);
296 - (void) setText: (NSString *)t
298 t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
299 whitespaceCharacterSet]];
300 t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
301 t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
302 t = [t stringByReplacingOccurrencesOfString:@">" withString:@">"];
303 t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
304 t = [t stringByReplacingOccurrencesOfString:@"<P> "
305 withString:@"<P> "];
306 t = [t stringByReplacingOccurrencesOfString:@"\n "
307 withString:@"<BR> "];
311 [t componentsSeparatedByCharactersInSet:
312 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
313 if ([s hasPrefix:@"http://"] ||
314 [s hasPrefix:@"https://"]) {
315 char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
316 NSString *a2 = [NSString stringWithCString: anchor
317 encoding: NSUTF8StringEncoding];
318 s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
321 h = [NSString stringWithFormat: @"%@ %@", h, s];
324 h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"];
325 h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"];
326 h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
327 whitespaceAndNewlineCharacterSet]];
333 -(BOOL) webView:(UIWebView *)wv
334 shouldStartLoadWithRequest:(NSURLRequest *)req
335 navigationType:(UIWebViewNavigationType)type
337 // Force clicked links to open in Safari, not in this window.
338 if (type == UIWebViewNavigationTypeLinkClicked) {
339 [[UIApplication sharedApplication] openURL:[req URL]];
346 - (void) setFrame: (CGRect)r
351 [webView setFrame: r];
355 - (NSString *) stripTags:(NSString *)str
357 NSString *result = @"";
360 str = [str stringByReplacingOccurrencesOfString:@"<P>"
361 withString:@"<BR><BR>"
362 options:NSCaseInsensitiveSearch
363 range:NSMakeRange(0, [str length])];
364 str = [str stringByReplacingOccurrencesOfString:@"<BR>"
366 options:NSCaseInsensitiveSearch
367 range:NSMakeRange(0, [str length])];
370 for (NSString *s in [str componentsSeparatedByString: @"<"]) {
371 NSRange r = [s rangeOfString:@">"];
373 s = [s substringFromIndex: r.location + r.length];
374 result = [result stringByAppendingString: s];
377 // Compress internal horizontal whitespace.
380 for (NSString *s in [str componentsSeparatedByCharactersInSet:
381 [NSCharacterSet whitespaceCharacterSet]]) {
382 if ([result length] == 0)
384 else if ([s length] > 0)
385 result = [NSString stringWithFormat: @"%@ %@", result, s];
394 CGRect r = [self frame];
396 /* It would be sensible to just ask the UIWebView how tall the page is,
397 instead of hoping that NSString and UIWebView measure fonts and do
398 wrapping in exactly the same way, but since UIWebView is asynchronous,
399 we'd have to wait for the document to load first, e.g.:
401 - Start the document loading;
402 - return a default height to use for the UITableViewCell;
403 - wait for the webViewDidFinishLoad delegate method to fire;
404 - then force the UITableView to reload, to pick up the new height.
406 But I couldn't make that work.
409 r.size.height = [[webView
410 stringByEvaluatingJavaScriptFromString:
411 @"document.body.offsetHeight"]
414 NSString *text = [self stripTags: html];
417 s = [text sizeWithFont: font
419 lineBreakMode:NSLineBreakByWordWrapping];
420 r.size.height = s.height;
437 #endif // USE_IPHONE && USE_HTML_LABELS
440 @interface XScreenSaverConfigSheet (Private)
442 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
445 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
446 - (void) placeChild: (NSView *)c on:(NSView *)p;
447 static NSView *last_child (NSView *parent);
448 static void layout_group (NSView *group, BOOL horiz_p);
450 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
451 - (void) placeChild: (NSObject *)c on:(NSView *)p;
452 - (void) placeSeparator;
453 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
454 - (void) refreshTableView;
455 # endif // USE_IPHONE
460 @implementation XScreenSaverConfigSheet
462 # define LEFT_MARGIN 20 // left edge of window
463 # define COLUMN_SPACING 10 // gap between e.g. labels and text fields
464 # define LEFT_LABEL_WIDTH 70 // width of all left labels
465 # define LINE_SPACING 10 // leading between each line
467 # define FONT_SIZE 17 // Magic hardcoded UITableView font size.
469 #pragma mark Talking to the resource database
472 /* Normally we read resources by looking up "KEY" in the database
473 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
474 app, everything is stored in the database "org.jwz.xscreensaver"
475 instead, so transform keys to "SAVERNAME.KEY".
477 NOTE: This is duplicated in PrefsReader.m, cause I suck.
479 - (NSString *) makeKey:(NSString *)key
482 NSString *prefix = [saver_name stringByAppendingString:@"."];
483 if (! [key hasPrefix:prefix]) // Don't double up!
484 key = [prefix stringByAppendingString:key];
490 - (NSString *) makeCKey:(const char *)key
492 return [self makeKey:[NSString stringWithCString:key
493 encoding:NSUTF8StringEncoding]];
497 /* Given a command-line option, returns the corresponding resource name.
498 Any arguments in the switch string are ignored (e.g., "-foo x").
500 - (NSString *) switchToResource:(NSString *)cmdline_switch
501 opts:(const XrmOptionDescRec *)opts_array
502 valRet:(NSString **)val_ret
506 NSAssert(cmdline_switch, @"cmdline switch is null");
507 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
508 encoding:NSUTF8StringEncoding]) {
509 NSAssert1(0, @"unable to convert %@", cmdline_switch);
512 char *s = strpbrk(buf, " \t\r\n");
516 while (*tail && (*tail == ' ' || *tail == '\t'))
520 while (opts_array[0].option) {
521 if (!strcmp (opts_array[0].option, buf)) {
524 if (opts_array[0].argKind == XrmoptionNoArg) {
526 NSAssert1 (0, @"expected no args to switch: \"%@\"",
528 ret = opts_array[0].value;
531 NSAssert1 (0, @"expected args to switch: \"%@\"",
538 ? [NSString stringWithCString:ret
539 encoding:NSUTF8StringEncoding]
542 const char *res = opts_array[0].specifier;
543 while (*res && (*res == '.' || *res == '*'))
545 return [self makeCKey:res];
550 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
555 - (NSUserDefaultsController *)controllerForKey:(NSString *)key
557 static NSDictionary *a = 0;
559 a = UPDATER_DEFAULTS;
562 if ([a objectForKey:key])
563 // These preferences are global to all xscreensavers.
564 return globalDefaultsController;
566 // All other preferences are per-saver.
567 return userDefaultsController;
573 // Called when a slider is bonked.
575 - (void)sliderAction:(UISlider*)sender
577 if ([active_text_field canResignFirstResponder])
578 [active_text_field resignFirstResponder];
579 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
581 // Hacky API. See comment in InvertedSlider.m.
582 double v = ([sender isKindOfClass: [InvertedSlider class]]
583 ? [(InvertedSlider *) sender transformedValue]
586 [[self controllerForKey:pref_key]
587 setObject:((v == (int) v)
588 ? [NSNumber numberWithInt:(int) v]
589 : [NSNumber numberWithDouble: v])
593 // Called when a checkbox/switch is bonked.
595 - (void)switchAction:(UISwitch*)sender
597 if ([active_text_field canResignFirstResponder])
598 [active_text_field resignFirstResponder];
599 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
600 NSString *v = ([sender isOn] ? @"true" : @"false");
601 [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
604 # ifdef USE_PICKER_VIEW
605 // Called when a picker is bonked.
607 - (void)pickerView:(UIPickerView *)pv
608 didSelectRow:(NSInteger)row
609 inComponent:(NSInteger)column
611 if ([active_text_field canResignFirstResponder])
612 [active_text_field resignFirstResponder];
614 NSAssert (column == 0, @"internal error");
615 NSArray *a = [picker_values objectAtIndex: [pv tag]];
616 if (! a) return; // Too early?
617 a = [a objectAtIndex:row];
618 NSAssert (a, @"missing row");
620 //NSString *label = [a objectAtIndex:0];
621 NSString *pref_key = [a objectAtIndex:1];
622 NSObject *pref_val = [a objectAtIndex:2];
623 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
625 # else // !USE_PICKER_VIEW
627 // Called when a RadioButton is bonked.
629 - (void)radioAction:(RadioButton*)sender
631 if ([active_text_field canResignFirstResponder])
632 [active_text_field resignFirstResponder];
634 NSArray *item = [[sender items] objectAtIndex: [sender index]];
635 NSString *pref_key = [item objectAtIndex:1];
636 NSObject *pref_val = [item objectAtIndex:2];
637 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
640 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
642 active_text_field = tf;
646 - (void)textFieldDidEndEditing:(UITextField *)tf
648 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
649 NSString *txt = [tf text];
650 [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
653 - (BOOL)textFieldShouldReturn:(UITextField *)tf
655 active_text_field = nil;
656 [tf resignFirstResponder];
660 # endif // !USE_PICKER_VIEW
667 - (void) okAction:(NSObject *)arg
669 [userDefaultsController commitEditing];
670 [globalDefaultsController commitEditing];
671 [userDefaultsController save:self];
672 [globalDefaultsController save:self];
673 [NSApp endSheet:self returnCode:NSOKButton];
677 - (void) cancelAction:(NSObject *)arg
679 [userDefaultsController revert:self];
680 [globalDefaultsController revert:self];
681 [NSApp endSheet:self returnCode:NSCancelButton];
684 # endif // !USE_IPHONE
687 - (void) resetAction:(NSObject *)arg
690 [userDefaultsController revertToInitialValues:self];
691 [globalDefaultsController revertToInitialValues:self];
694 for (NSString *key in defaultOptions) {
695 NSObject *val = [defaultOptions objectForKey:key];
696 [[self controllerForKey:key] setObject:val forKey:key];
699 for (UIControl *ctl in pref_ctls) {
700 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
701 [self bindResource:ctl key:pref_key reload:YES];
704 [self refreshTableView];
705 # endif // USE_IPHONE
709 /* Connects a control (checkbox, etc) to the corresponding preferences key.
711 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
712 reload:(BOOL)reload_p
714 NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
716 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
718 : ([control isKindOfClass:[NSMatrix class]]
723 withKeyPath:[@"values." stringByAppendingString: pref_key]
727 NSObject *val = [prefs objectForKey:pref_key];
731 if ([val isKindOfClass:[NSString class]]) {
732 sval = (NSString *) val;
733 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
734 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
735 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
738 dval = [sval doubleValue];
739 } else if ([val isKindOfClass:[NSNumber class]]) {
740 // NSBoolean (__NSCFBoolean) is really NSNumber.
741 dval = [(NSNumber *) val doubleValue];
742 sval = [(NSNumber *) val stringValue];
745 if ([control isKindOfClass:[UISlider class]]) {
746 sel = @selector(sliderAction:);
747 // Hacky API. See comment in InvertedSlider.m.
748 if ([control isKindOfClass:[InvertedSlider class]])
749 [(InvertedSlider *) control setTransformedValue: dval];
751 [(UISlider *) control setValue: dval];
752 } else if ([control isKindOfClass:[UISwitch class]]) {
753 sel = @selector(switchAction:);
754 [(UISwitch *) control setOn: ((int) dval != 0)];
755 # ifdef USE_PICKER_VIEW
756 } else if ([control isKindOfClass:[UIPickerView class]]) {
758 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
760 # else // !USE_PICKER_VIEW
761 } else if ([control isKindOfClass:[RadioButton class]]) {
762 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
763 } else if ([control isKindOfClass:[UITextField class]]) {
765 [(UITextField *) control setText: sval];
766 # endif // !USE_PICKER_VIEW
768 NSAssert (0, @"unknown class");
771 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
775 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
776 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
779 [pref_keys addObject: [self makeKey:pref_key]];
780 [pref_ctls addObject: control];
781 ((UIControl *) control).tag = [pref_keys count] - 1;
784 [(UIControl *) control addTarget:self action:sel
785 forControlEvents:UIControlEventValueChanged];
789 # endif // USE_IPHONE
792 NSObject *def = [[prefs defaults] objectForKey:pref_key];
793 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
794 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
795 s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def];
796 s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0];
797 NSLog (@"%@ %@/%@", s, [def class], [control class]);
802 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
804 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
809 - (void) bindSwitch:(NSObject *)control
810 cmdline:(NSString *)cmd
812 [self bindResource:control
813 key:[self switchToResource:cmd opts:opts valRet:0]];
817 #pragma mark Text-manipulating utilities
821 unwrap (NSString *text)
823 // Unwrap lines: delete \n but do not delete \n\n.
825 NSArray *lines = [text componentsSeparatedByString:@"\n"];
826 NSUInteger i, nlines = [lines count];
829 text = @"\n"; // start with one blank line
831 // skip trailing blank lines in file
832 for (i = nlines-1; i > 0; i--) {
833 NSString *s = (NSString *) [lines objectAtIndex:i];
839 // skip leading blank lines in file
840 for (i = 0; i < nlines; i++) {
841 NSString *s = (NSString *) [lines objectAtIndex:i];
848 for (; i < nlines; i++) {
849 NSString *s = (NSString *) [lines objectAtIndex:i];
850 if ([s length] == 0) {
851 text = [text stringByAppendingString:@"\n\n"];
853 } else if ([s characterAtIndex:0] == ' ' ||
854 [s hasPrefix:@"Copyright "] ||
855 [s hasPrefix:@"http://"]) {
856 // don't unwrap if the following line begins with whitespace,
857 // or with the word "Copyright", or if it begins with a URL.
859 text = [text stringByAppendingString:@"\n"];
860 text = [text stringByAppendingString:s];
865 text = [text stringByAppendingString:@" "];
866 text = [text stringByAppendingString:s];
877 /* Makes the text up to the first comma be bold.
880 boldify (NSText *nstext)
882 NSString *text = [nstext string];
883 NSRange r = [text rangeOfString:@"," options:0];
884 r.length = r.location+1;
888 NSFont *font = [nstext font];
889 font = [NSFont boldSystemFontOfSize:[font pointSize]];
890 [nstext setFont:font range:r];
892 # endif // !USE_IPHONE
895 /* Creates a human-readable anchor to put on a URL.
898 anchorize (const char *url)
900 const char *wiki = "http://en.wikipedia.org/wiki/";
901 const char *math = "http://mathworld.wolfram.com/";
902 if (!strncmp (wiki, url, strlen(wiki))) {
903 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
904 strcpy (anchor, "Wikipedia: \"");
905 const char *in = url + strlen(wiki);
906 char *out = anchor + strlen(anchor);
910 } else if (*in == '#') {
913 } else if (*in == '%') {
919 sscanf (hex, "%x", &n);
931 } else if (!strncmp (math, url, strlen(math))) {
932 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
933 strcpy (anchor, "MathWorld: \"");
934 const char *start = url + strlen(wiki);
935 const char *in = start;
936 char *out = anchor + strlen(anchor);
940 } else if (in != start && *in >= 'A' && *in <= 'Z') {
943 } else if (!strncmp (in, ".htm", 4)) {
960 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
962 /* Converts any http: URLs in the given text field to clickable links.
965 hreffify (NSText *nstext)
968 NSString *text = [nstext string];
969 [nstext setRichText:YES];
971 NSString *text = [nstext text];
974 int L = [text length];
975 NSRange start; // range is start-of-search to end-of-string
978 while (start.location < L) {
980 // Find the beginning of a URL...
982 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
983 if (r2.location == NSNotFound)
986 // Next time around, start searching after this.
987 start.location = r2.location + r2.length;
988 start.length = L - start.location;
990 // Find the end of a URL (whitespace or EOF)...
992 NSRange r3 = [text rangeOfCharacterFromSet:
993 [NSCharacterSet whitespaceAndNewlineCharacterSet]
994 options:0 range:start];
995 if (r3.location == NSNotFound) // EOF
996 r3.location = L, r3.length = 0;
998 // Next time around, start searching after this.
999 start.location = r3.location;
1000 start.length = L - start.location;
1002 // Set r2 to the start/length of this URL.
1003 r2.length = start.location - r2.location;
1006 NSString *nsurl = [text substringWithRange:r2];
1007 const char *url = [nsurl UTF8String];
1009 // If this is a Wikipedia URL, make the linked text be prettier.
1011 char *anchor = anchorize(url);
1015 // Construct the RTF corresponding to <A HREF="url">anchor</A>
1017 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1018 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1019 sprintf (rtf, fmt, url, anchor);
1021 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1022 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1024 # else // !USE_IPHONE
1025 // *anchor = 0; // Omit Wikipedia anchor
1026 text = [text stringByReplacingCharactersInRange:r2
1027 withString:[NSString stringWithCString:anchor
1028 encoding:NSUTF8StringEncoding]];
1029 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1030 // withString:@"\n\n"];
1031 # endif // !USE_IPHONE
1035 int L2 = [text length]; // might have changed
1036 start.location -= (L - L2);
1041 [nstext setText:text];
1046 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1050 #pragma mark Creating controls from XML
1053 /* Parse the attributes of an XML tag into a dictionary.
1054 For input, the dictionary should have as attributes the keys, each
1055 with @"" as their value.
1056 On output, the dictionary will set the keys to the values specified,
1057 and keys that were not specified will not be present in the dictionary.
1058 Warnings are printed if there are duplicate or unknown attributes.
1060 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1062 NSArray *attrs = [(NSXMLElement *) node attributes];
1063 NSUInteger n = [attrs count];
1066 // For each key in the dictionary, fill in the dict with the corresponding
1067 // value. The value @"" is assumed to mean "un-set". Issue a warning if
1068 // an attribute is specified twice.
1070 for (i = 0; i < n; i++) {
1071 NSXMLNode *attr = [attrs objectAtIndex:i];
1072 NSString *key = [attr name];
1073 NSString *val = [attr objectValue];
1074 NSString *old = [dict objectForKey:key];
1077 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1078 } else if ([old length] != 0) {
1079 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1081 [dict setValue:val forKey:key];
1085 // Remove from the dictionary any keys whose value is still @"",
1086 // meaning there was no such attribute specified.
1088 NSArray *keys = [dict allKeys];
1090 for (i = 0; i < n; i++) {
1091 NSString *key = [keys objectAtIndex:i];
1092 NSString *val = [dict objectForKey:key];
1093 if ([val length] == 0)
1094 [dict removeObjectForKey:key];
1098 // Kludge for starwars.xml:
1099 // If there is a "_low-label" and no "_label", but "_low-label" contains
1100 // spaces, divide them.
1101 NSString *lab = [dict objectForKey:@"_label"];
1102 NSString *low = [dict objectForKey:@"_low-label"];
1105 [[[low stringByTrimmingCharactersInSet:
1106 [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1107 componentsSeparatedByString: @" "]
1108 filteredArrayUsingPredicate:
1109 [NSPredicate predicateWithFormat:@"length > 0"]];
1110 if (split && [split count] == 2) {
1111 [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1112 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1115 # endif // USE_IPHONE
1119 /* Handle the options on the top level <xscreensaver> tag.
1121 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1123 NSMutableDictionary *dict = [@{ @"name": @"",
1127 [self parseAttrs:dict node:node];
1128 NSString *name = [dict objectForKey:@"name"];
1129 NSString *label = [dict objectForKey:@"_label"];
1131 NSAssert1 (label, @"no _label in %@", [node name]);
1132 NSAssert1 (name, @"no name in \"%@\"", label);
1137 /* Creates a label: an un-editable NSTextField displaying the given text.
1139 - (LABEL *) makeLabel:(NSString *)text
1142 rect.origin.x = rect.origin.y = 0;
1143 rect.size.width = rect.size.height = 10;
1145 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1146 [lab setSelectable:NO];
1147 [lab setEditable:NO];
1148 [lab setBezeled:NO];
1149 [lab setDrawsBackground:NO];
1150 [lab setStringValue:text];
1152 # else // USE_IPHONE
1153 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1154 [lab setText: [text stringByTrimmingCharactersInSet:
1155 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1156 [lab setBackgroundColor:[UIColor clearColor]];
1157 [lab setNumberOfLines:0]; // unlimited
1158 // [lab setLineBreakMode:UILineBreakModeWordWrap];
1159 [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1160 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1161 UIViewAutoresizingFlexibleHeight)];
1162 # endif // USE_IPHONE
1167 /* Creates the checkbox (NSButton) described by the given XML node.
1169 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1171 NSMutableDictionary *dict = [@{ @"id": @"",
1176 [self parseAttrs:dict node:node];
1177 NSString *label = [dict objectForKey:@"_label"];
1178 NSString *arg_set = [dict objectForKey:@"arg-set"];
1179 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1182 NSAssert1 (0, @"no _label in %@", [node name]);
1185 if (!arg_set && !arg_unset) {
1186 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
1189 if (arg_set && arg_unset) {
1190 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
1194 // sanity-check the choice of argument names.
1196 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1197 [arg_set hasPrefix:@"--no-"]))
1198 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1200 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1201 ![arg_unset hasPrefix:@"--no-"]))
1202 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1206 rect.origin.x = rect.origin.y = 0;
1207 rect.size.width = rect.size.height = 10;
1211 NSButton *button = [[NSButton alloc] initWithFrame:rect];
1212 [button setButtonType:NSSwitchButton];
1213 [button setTitle:label];
1215 [self placeChild:button on:parent];
1217 # else // USE_IPHONE
1219 LABEL *lab = [self makeLabel:label];
1220 [self placeChild:lab on:parent];
1221 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1222 [self placeChild:button on:parent right:YES];
1225 # endif // USE_IPHONE
1227 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1232 /* Creates the number selection control described by the given XML node.
1233 If "type=slider", it's an NSSlider.
1234 If "type=spinbutton", it's a text field with up/down arrows next to it.
1236 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1238 NSMutableDictionary *dict = [@{ @"id": @"",
1241 @"_high-label": @"",
1249 [self parseAttrs:dict node:node];
1250 NSString *label = [dict objectForKey:@"_label"];
1251 NSString *low_label = [dict objectForKey:@"_low-label"];
1252 NSString *high_label = [dict objectForKey:@"_high-label"];
1253 NSString *type = [dict objectForKey:@"type"];
1254 NSString *arg = [dict objectForKey:@"arg"];
1255 NSString *low = [dict objectForKey:@"low"];
1256 NSString *high = [dict objectForKey:@"high"];
1257 NSString *def = [dict objectForKey:@"default"];
1258 NSString *cvt = [dict objectForKey:@"convert"];
1260 NSAssert1 (arg, @"no arg in %@", label);
1261 NSAssert1 (type, @"no type in %@", label);
1264 NSAssert1 (0, @"no low in %@", [node name]);
1268 NSAssert1 (0, @"no high in %@", [node name]);
1272 NSAssert1 (0, @"no default in %@", [node name]);
1275 if (cvt && ![cvt isEqualToString:@"invert"]) {
1276 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1280 // If either the min or max field contains a decimal point, then this
1281 // option may have a floating point value; otherwise, it is constrained
1282 // to be an integer.
1284 NSCharacterSet *dot =
1285 [NSCharacterSet characterSetWithCharactersInString:@"."];
1286 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1287 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1289 if ([type isEqualToString:@"slider"]
1290 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1291 || [type isEqualToString:@"spinbutton"]
1296 rect.origin.x = rect.origin.y = 0;
1297 rect.size.width = 150;
1298 rect.size.height = 23; // apparent min height for slider with ticks...
1300 slider = [[InvertedSlider alloc] initWithFrame:rect
1302 integers: !float_p];
1303 [slider setMaxValue:[high doubleValue]];
1304 [slider setMinValue:[low doubleValue]];
1306 int range = [slider maxValue] - [slider minValue] + 1;
1309 while (range2 > max_ticks)
1312 // If we have elided ticks, leave it at the max number of ticks.
1313 if (range != range2 && range2 < max_ticks)
1316 // If it's a float, always display the max number of ticks.
1317 if (float_p && range2 < max_ticks)
1321 [slider setNumberOfTickMarks:range2];
1323 [slider setAllowsTickMarkValuesOnly:
1324 (range == range2 && // we are showing the actual number of ticks
1325 !float_p)]; // and we want integer results
1326 # endif // !USE_IPHONE
1328 // #### Note: when the slider's range is large enough that we aren't
1329 // showing all possible ticks, the slider's value is not constrained
1330 // to be an integer, even though it should be...
1331 // Maybe we need to use a value converter or something?
1335 lab = [self makeLabel:label];
1336 [self placeChild:lab on:parent];
1339 CGFloat s = [NSFont systemFontSize] + 4;
1340 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1347 lab = [self makeLabel:low_label];
1348 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1350 [lab setAlignment:1]; // right aligned
1352 if (rect.size.width < LEFT_LABEL_WIDTH)
1353 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1354 rect.size.height = [slider frame].size.height;
1355 [lab setFrame:rect];
1356 [self placeChild:lab on:parent];
1357 # else // USE_IPHONE
1358 [lab setTextAlignment: NSTextAlignmentRight];
1359 // Sometimes rotation screws up truncation.
1360 [lab setLineBreakMode:NSLineBreakByClipping];
1361 [self placeChild:lab on:parent right:(label ? YES : NO)];
1362 # endif // USE_IPHONE
1368 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1369 # else // USE_IPHONE
1370 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1371 # endif // USE_IPHONE
1374 // Make left label be same height as slider.
1376 rect.size.height = [slider frame].size.height;
1377 [lab setFrame:rect];
1381 rect = [slider frame];
1382 if (rect.origin.x < LEFT_LABEL_WIDTH)
1383 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1384 [slider setFrame:rect];
1388 lab = [self makeLabel:high_label];
1389 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1392 // Make right label be same height as slider.
1393 rect.size.height = [slider frame].size.height;
1394 [lab setFrame:rect];
1396 // Sometimes rotation screws up truncation.
1397 [lab setLineBreakMode:NSLineBreakByClipping];
1399 [self placeChild:lab on:parent right:YES];
1403 [self bindSwitch:slider cmdline:arg];
1406 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1408 } else if ([type isEqualToString:@"spinbutton"]) {
1411 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1414 NSAssert1 (!low_label,
1415 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1416 NSAssert1 (!high_label,
1417 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1418 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1422 rect.origin.x = rect.origin.y = 0;
1423 rect.size.width = rect.size.height = 10;
1425 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1426 [txt setStringValue:@"0000.0"];
1428 [txt setStringValue:@""];
1431 LABEL *lab = [self makeLabel:label];
1432 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1433 [lab setAlignment:1]; // right aligned
1435 if (rect.size.width < LEFT_LABEL_WIDTH)
1436 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1437 rect.size.height = [txt frame].size.height;
1438 [lab setFrame:rect];
1439 [self placeChild:lab on:parent];
1443 [self placeChild:txt on:parent right:(label ? YES : NO)];
1447 if (rect.origin.x < LEFT_LABEL_WIDTH)
1448 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1449 [txt setFrame:rect];
1452 rect.size.width = rect.size.height = 10;
1453 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1455 [self placeChild:step on:parent right:YES];
1456 rect = [step frame];
1457 rect.origin.x -= COLUMN_SPACING; // this one goes close
1458 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1459 [step setFrame:rect];
1461 [step setMinValue:[low doubleValue]];
1462 [step setMaxValue:[high doubleValue]];
1463 [step setAutorepeat:YES];
1464 [step setValueWraps:NO];
1466 double range = [high doubleValue] - [low doubleValue];
1468 [step setIncrement:range / 10.0];
1469 else if (range >= 500)
1470 [step setIncrement:range / 100.0];
1472 [step setIncrement:1.0];
1474 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1475 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1476 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1477 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1478 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1479 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1480 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1482 [fmt setGeneratesDecimalNumbers:float_p];
1483 [[txt cell] setFormatter:fmt];
1485 [self bindSwitch:step cmdline:arg];
1486 [self bindSwitch:txt cmdline:arg];
1491 # endif // USE_IPHONE
1494 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1501 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1503 /* If the object associated with this menu item looks like a boolean,
1504 store an NSNumber instead of an NSString, since that's what
1505 will be in the preferences (due to similar logic in PrefsReader).
1507 if ([obj isKindOfClass:[NSString class]]) {
1508 NSString *string = (NSString *) obj;
1509 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1510 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1511 obj = [NSNumber numberWithBool:YES];
1512 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1513 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1514 obj = [NSNumber numberWithBool:NO];
1519 [item setRepresentedObject:obj];
1520 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1522 # endif // !USE_IPHONE
1525 /* Creates the popup menu described by the given XML node (and its children).
1527 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1529 NSArray *children = [node children];
1530 NSUInteger i, count = [children count];
1533 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1537 // get the "id" attribute off the <select> tag.
1539 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1540 [self parseAttrs:dict node:node];
1543 rect.origin.x = rect.origin.y = 0;
1544 rect.size.width = 10;
1545 rect.size.height = 10;
1547 NSString *menu_key = nil; // the resource key used by items in this menu
1550 // #### "Build and Analyze" says that all of our widgets leak, because it
1551 // seems to not realize that placeChild -> addSubview retains them.
1552 // Not sure what to do to make these warnings go away.
1554 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1556 NSMenuItem *def_item = nil;
1557 float max_width = 0;
1559 # else // USE_IPHONE
1561 NSString *def_item = nil;
1563 rect.size.width = 0;
1564 rect.size.height = 0;
1565 # ifdef USE_PICKER_VIEW
1566 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1567 popup.delegate = self;
1568 popup.dataSource = self;
1569 # endif // !USE_PICKER_VIEW
1570 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1572 # endif // USE_IPHONE
1574 for (i = 0; i < count; i++) {
1575 NSXMLNode *child = [children objectAtIndex:i];
1577 if ([child kind] == NSXMLCommentKind)
1579 if ([child kind] != NSXMLElementKind) {
1580 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1584 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1586 NSMutableDictionary *dict2 = [@{ @"id": @"",
1590 [self parseAttrs:dict2 node:child];
1591 NSString *label = [dict2 objectForKey:@"_label"];
1592 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1595 NSAssert1 (0, @"no _label in %@", [child name]);
1600 // create the menu item (and then get a pointer to it)
1601 [popup addItemWithTitle:label];
1602 NSMenuItem *item = [popup itemWithTitle:label];
1603 # endif // USE_IPHONE
1606 NSString *this_val = NULL;
1607 NSString *this_key = [self switchToResource: arg_set
1610 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1611 if (menu_key && ![menu_key isEqualToString:this_key])
1613 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1614 menu_key, this_key, this_val);
1616 menu_key = this_key;
1618 /* If this menu has the cmd line "-mode foo" then set this item's
1619 value to "foo" (the menu itself will be bound to e.g. "modeString")
1622 set_menu_item_object (item, this_val);
1624 // Array holds ["Label", "resource-key", "resource-val"].
1625 [items addObject:[NSMutableArray arrayWithObjects:
1626 label, @"", this_val, nil]];
1630 // no arg-set -- only one menu item can be missing that.
1631 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1636 // Array holds ["Label", "resource-key", "resource-val"].
1637 [items addObject:[NSMutableArray arrayWithObjects:
1638 label, @"", @"", nil]];
1642 /* make sure the menu button has room for the text of this item,
1643 and remember the greatest width it has reached.
1646 [popup setTitle:label];
1648 NSRect r = [popup frame];
1649 if (r.size.width > max_width) max_width = r.size.width;
1650 # endif // USE_IPHONE
1654 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1658 /* We've added all of the menu items. If there was an item with no
1659 command-line switch, then it's the item that represents the default
1660 value. Now we must bind to that item as well... (We have to bind
1661 this one late, because if it was the first item, then we didn't
1662 yet know what resource was associated with this menu.)
1665 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1667 @"no default value for resource \"%@\" in menu item \"%@\"",
1677 set_menu_item_object (def_item, def_obj);
1678 # else // !USE_IPHONE
1679 for (NSMutableArray *a in items) {
1680 // Make sure each array contains the resource key.
1681 [a replaceObjectAtIndex:1 withObject:menu_key];
1682 // Make sure the default item contains the default resource value.
1683 if (def_obj && def_item &&
1684 [def_item isEqualToString:[a objectAtIndex:0]])
1685 [a replaceObjectAtIndex:2 withObject:def_obj];
1687 # endif // !USE_IPHONE
1691 # ifdef USE_PICKER_VIEW
1692 /* Finish tweaking the menu button itself.
1695 [popup setTitle:[def_item title]];
1696 NSRect r = [popup frame];
1697 r.size.width = max_width;
1699 # endif // USE_PICKER_VIEW
1702 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1703 [self placeChild:popup on:parent];
1704 [self bindResource:popup key:menu_key];
1709 # ifdef USE_PICKER_VIEW
1710 // Store the items for this picker in the picker_values array.
1711 // This is so fucking stupid.
1713 unsigned long menu_number = [pref_keys count] - 1;
1714 if (! picker_values)
1715 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1716 while ([picker_values count] <= menu_number)
1717 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1718 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1719 [popup reloadAllComponents];
1721 # else // !USE_PICKER_VIEW
1723 [self placeSeparator];
1726 for (__attribute__((unused)) NSArray *item in items) {
1727 RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1729 [b setLineBreakMode:NSLineBreakByTruncatingHead];
1730 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1731 [self placeChild:b on:parent];
1735 [self placeSeparator];
1737 # endif // !USE_PICKER_VIEW
1738 # endif // !USE_IPHONE
1743 /* Creates an uneditable, wrapping NSTextField to display the given
1744 text enclosed by <description> ... </description> in the XML.
1746 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1748 NSString *text = nil;
1749 NSArray *children = [node children];
1750 NSUInteger i, count = [children count];
1752 for (i = 0; i < count; i++) {
1753 NSXMLNode *child = [children objectAtIndex:i];
1754 NSString *s = [child objectValue];
1756 text = [text stringByAppendingString:s];
1761 text = unwrap (text);
1763 NSRect rect = [parent frame];
1764 rect.origin.x = rect.origin.y = 0;
1765 rect.size.width = 200;
1766 rect.size.height = 50; // sized later
1768 NSText *lab = [[NSText alloc] initWithFrame:rect];
1769 [lab setEditable:NO];
1770 [lab setDrawsBackground:NO];
1771 [lab setHorizontallyResizable:YES];
1772 [lab setVerticallyResizable:YES];
1773 [lab setString:text];
1778 # else // USE_IPHONE
1780 # ifndef USE_HTML_LABELS
1782 UILabel *lab = [self makeLabel:text];
1783 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1786 # else // USE_HTML_LABELS
1787 HTMLLabel *lab = [[HTMLLabel alloc]
1789 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1790 [lab setFrame:rect];
1792 # endif // USE_HTML_LABELS
1794 [self placeSeparator];
1796 # endif // USE_IPHONE
1798 [self placeChild:lab on:parent];
1803 /* Creates the NSTextField described by the given XML node.
1805 - (void) makeTextField: (NSXMLNode *)node
1806 on: (NSView *)parent
1807 withLabel: (BOOL) label_p
1808 horizontal: (BOOL) horiz_p
1810 NSMutableDictionary *dict = [@{ @"id": @"",
1814 [self parseAttrs:dict node:node];
1815 NSString *label = [dict objectForKey:@"_label"];
1816 NSString *arg = [dict objectForKey:@"arg"];
1818 if (!label && label_p) {
1819 NSAssert1 (0, @"no _label in %@", [node name]);
1823 NSAssert1 (arg, @"no arg in %@", label);
1826 rect.origin.x = rect.origin.y = 0;
1827 rect.size.width = rect.size.height = 10;
1829 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1833 // make the default size be around 30 columns; a typical value for
1834 // these text fields is "xscreensaver-text --cols 40".
1836 [txt setStringValue:@"123456789 123456789 123456789 "];
1838 [[txt cell] setWraps:NO];
1839 [[txt cell] setScrollable:YES];
1840 [txt setStringValue:@""];
1842 # else // USE_IPHONE
1844 txt.adjustsFontSizeToFitWidth = YES;
1845 txt.textColor = [UIColor blackColor];
1846 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1847 txt.placeholder = @"";
1848 txt.borderStyle = UITextBorderStyleRoundedRect;
1849 txt.textAlignment = NSTextAlignmentRight;
1850 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1851 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1852 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1853 txt.clearButtonMode = UITextFieldViewModeAlways;
1854 txt.returnKeyType = UIReturnKeyDone;
1855 txt.delegate = self;
1857 [txt setEnabled: YES];
1859 rect.size.height = [txt.font lineHeight] * 1.2;
1860 [txt setFrame:rect];
1862 # endif // USE_IPHONE
1865 LABEL *lab = [self makeLabel:label];
1866 [self placeChild:lab on:parent];
1870 [self placeChild:txt on:parent right:(label ? YES : NO)];
1872 [self bindSwitch:txt cmdline:arg];
1877 /* Creates the NSTextField described by the given XML node,
1878 and hooks it up to a Choose button and a file selector widget.
1880 - (void) makeFileSelector: (NSXMLNode *)node
1881 on: (NSView *)parent
1882 dirsOnly: (BOOL) dirsOnly
1883 withLabel: (BOOL) label_p
1884 editable: (BOOL) editable_p
1886 # ifndef USE_IPHONE // No files. No selectors.
1887 NSMutableDictionary *dict = [@{ @"id": @"",
1891 [self parseAttrs:dict node:node];
1892 NSString *label = [dict objectForKey:@"_label"];
1893 NSString *arg = [dict objectForKey:@"arg"];
1895 if (!label && label_p) {
1896 NSAssert1 (0, @"no _label in %@", [node name]);
1900 NSAssert1 (arg, @"no arg in %@", label);
1903 rect.origin.x = rect.origin.y = 0;
1904 rect.size.width = rect.size.height = 10;
1906 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1908 // make the default size be around 20 columns.
1910 [txt setStringValue:@"123456789 123456789 "];
1912 [txt setSelectable:YES];
1913 [txt setEditable:editable_p];
1914 [txt setBezeled:editable_p];
1915 [txt setDrawsBackground:editable_p];
1916 [[txt cell] setWraps:NO];
1917 [[txt cell] setScrollable:YES];
1918 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1919 [txt setStringValue:@""];
1923 lab = [self makeLabel:label];
1924 [self placeChild:lab on:parent];
1928 [self placeChild:txt on:parent right:(label ? YES : NO)];
1930 [self bindSwitch:txt cmdline:arg];
1933 // Make the text field and label be the same height, whichever is taller.
1936 rect.size.height = ([lab frame].size.height > [txt frame].size.height
1937 ? [lab frame].size.height
1938 : [txt frame].size.height);
1939 [txt setFrame:rect];
1942 // Now put a "Choose" button next to it.
1944 rect.origin.x = rect.origin.y = 0;
1945 rect.size.width = rect.size.height = 10;
1946 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
1947 [choose setTitle:@"Choose..."];
1948 [choose setBezelStyle:NSRoundedBezelStyle];
1951 [self placeChild:choose on:parent right:YES];
1953 // center the Choose button around the midpoint of the text field.
1954 rect = [choose frame];
1955 rect.origin.y = ([txt frame].origin.y +
1956 (([txt frame].size.height - rect.size.height) / 2));
1957 [choose setFrameOrigin:rect.origin];
1959 [choose setTarget:[parent window]];
1961 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
1963 [choose setAction:@selector(fileSelectorChooseAction:)];
1966 # endif // !USE_IPHONE
1972 /* Runs a modal file selector and sets the text field's value to the
1973 selected file or directory.
1976 do_file_selector (NSTextField *txt, BOOL dirs_p)
1978 NSOpenPanel *panel = [NSOpenPanel openPanel];
1979 [panel setAllowsMultipleSelection:NO];
1980 [panel setCanChooseFiles:!dirs_p];
1981 [panel setCanChooseDirectories:dirs_p];
1983 NSString *file = [txt stringValue];
1984 if ([file length] <= 0) {
1985 file = NSHomeDirectory();
1987 file = [file stringByAppendingPathComponent:@"Pictures"];
1990 // NSString *dir = [file stringByDeletingLastPathComponent];
1992 int result = [panel runModalForDirectory:file //dir
1993 file:nil //[file lastPathComponent]
1995 if (result == NSOKButton) {
1996 NSArray *files = [panel filenames];
1997 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
1998 file = [file stringByAbbreviatingWithTildeInPath];
1999 [txt setStringValue:file];
2001 // Fuck me! Just setting the value of the NSTextField does not cause
2002 // that to end up in the preferences!
2004 NSDictionary *dict = [txt infoForBinding:@"value"];
2005 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
2006 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
2007 if ([path hasPrefix:@"values."]) // WTF.
2008 path = [path substringFromIndex:7];
2009 [[prefs values] setValue:file forKey:path];
2012 // make sure the end of the string is visible.
2013 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
2015 range.location = [file length]-3;
2017 if (! [[txt window] makeFirstResponder:[txt window]])
2018 [[txt window] endEditingFor:nil];
2019 // [[txt window] makeFirstResponder:nil];
2020 [fe setSelectedRange:range];
2021 // [tv scrollRangeToVisible:range];
2022 // [txt setNeedsDisplay:YES];
2023 // [[txt cell] setNeedsDisplay:YES];
2024 // [txt selectAll:txt];
2030 /* Returns the NSTextField that is to the left of or above the NSButton.
2032 static NSTextField *
2033 find_text_field_of_button (NSButton *button)
2035 NSView *parent = [button superview];
2036 NSArray *kids = [parent subviews];
2037 int nkids = [kids count];
2040 for (i = 0; i < nkids; i++) {
2041 NSObject *kid = [kids objectAtIndex:i];
2042 if ([kid isKindOfClass:[NSTextField class]]) {
2043 f = (NSTextField *) kid;
2044 } else if (kid == button) {
2053 - (void) fileSelectorChooseAction:(NSObject *)arg
2055 NSButton *choose = (NSButton *) arg;
2056 NSTextField *txt = find_text_field_of_button (choose);
2057 do_file_selector (txt, NO);
2060 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2062 NSButton *choose = (NSButton *) arg;
2063 NSTextField *txt = find_text_field_of_button (choose);
2064 do_file_selector (txt, YES);
2067 #endif // !USE_IPHONE
2070 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2075 (x) Computer name and time
2076 ( ) Text [__________________________]
2077 ( ) Text file [_________________] [Choose]
2078 ( ) URL [__________________________]
2079 ( ) Shell Cmd [__________________________]
2081 textMode -text-mode date
2082 textMode -text-mode literal textLiteral -text-literal %
2083 textMode -text-mode file textFile -text-file %
2084 textMode -text-mode url textURL -text-url %
2085 textMode -text-mode program textProgram -text-program %
2088 rect.size.width = rect.size.height = 1;
2089 rect.origin.x = rect.origin.y = 0;
2090 NSView *group = [[NSView alloc] initWithFrame:rect];
2091 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2093 Bool program_p = TRUE;
2098 // This is how you link radio buttons together.
2100 NSButtonCell *proto = [[NSButtonCell alloc] init];
2101 [proto setButtonType:NSRadioButton];
2103 rect.origin.x = rect.origin.y = 0;
2104 rect.size.width = rect.size.height = 10;
2105 NSMatrix *matrix = [[NSMatrix alloc]
2107 mode:NSRadioModeMatrix
2109 numberOfRows: 4 + (program_p ? 1 : 0)
2111 [matrix setAllowsEmptySelection:NO];
2113 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
2114 [cnames addObject:@"Computer name and time"];
2115 [cnames addObject:@"Text"];
2116 [cnames addObject:@"File"];
2117 [cnames addObject:@"URL"];
2118 if (program_p) [cnames addObject:@"Shell Cmd"];
2119 [matrix bind:@"content"
2121 withKeyPath:@"arrangedObjects"
2125 [self bindSwitch:matrix cmdline:@"-text-mode %"];
2127 [self placeChild:matrix on:group];
2128 [self placeChild:rgroup on:group right:YES];
2132 # else // USE_IPHONE
2134 NSView *rgroup = parent;
2137 // <select id="textMode">
2138 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
2139 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
2140 // <option id="url" _label="Display URL"/>
2143 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2144 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2146 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2147 [node3 setAttributesAsDictionary:
2149 @"arg-set": @"-text-mode date",
2150 @"_label": @"Display the date and time" }];
2151 [node3 setParent: node2];
2154 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2155 [node3 setAttributesAsDictionary:
2157 @"arg-set": @"-text-mode literal",
2158 @"_label": @"Display static text" }];
2159 [node3 setParent: node2];
2162 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2163 [node3 setAttributesAsDictionary:
2165 @"_label": @"Display the contents of a URL" }];
2166 [node3 setParent: node2];
2169 [self makeOptionMenu:node2 on:rgroup];
2171 # endif // USE_IPHONE
2174 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2175 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2176 [node2 setAttributesAsDictionary:
2177 @{ @"id": @"textLiteral",
2178 @"arg": @"-text-literal %",
2180 @"_label": @"Text to display"
2183 [self makeTextField:node2 on:rgroup
2191 // rect = [last_child(rgroup) frame];
2193 /* // trying to make the text fields be enabled only when the checkbox is on..
2194 control = last_child (rgroup);
2195 [control bind:@"enabled"
2196 toObject:[matrix cellAtRow:1 column:0]
2197 withKeyPath:@"value"
2203 // <file id="textFile" _label="" arg-set="-text-file %"/>
2204 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2205 [node2 setAttributesAsDictionary:
2206 @{ @"id": @"textFile",
2207 @"arg": @"-text-file %" }];
2208 [self makeFileSelector:node2 on:rgroup
2209 dirsOnly:NO withLabel:NO editable:NO];
2210 # endif // !USE_IPHONE
2212 // rect = [last_child(rgroup) frame];
2214 // <string id="textURL" _label="" arg-set="text-url %"/>
2215 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2216 [node2 setAttributesAsDictionary:
2217 @{ @"id": @"textURL",
2218 @"arg": @"-text-url %",
2220 @"_label": @"URL to display",
2223 [self makeTextField:node2 on:rgroup
2231 // rect = [last_child(rgroup) frame];
2235 // <string id="textProgram" _label="" arg-set="text-program %"/>
2236 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2237 [node2 setAttributesAsDictionary:
2238 @{ @"id": @"textProgram",
2239 @"arg": @"-text-program %",
2241 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2244 // rect = [last_child(rgroup) frame];
2246 layout_group (rgroup, NO);
2248 rect = [rgroup frame];
2249 rect.size.width += 35; // WTF? Why is rgroup too narrow?
2250 [rgroup setFrame:rect];
2253 // Set the height of the cells in the radio-box matrix to the height of
2254 // the (last of the) text fields.
2255 control = last_child (rgroup);
2256 rect = [control frame];
2257 rect.size.width = 30; // width of the string "Text", plus a bit...
2259 rect.size.width += 25;
2260 rect.size.height += LINE_SPACING;
2261 [matrix setCellSize:rect.size];
2262 [matrix sizeToCells];
2264 layout_group (group, YES);
2265 rect = [matrix frame];
2266 rect.origin.x += rect.size.width + COLUMN_SPACING;
2267 rect.origin.y -= [control frame].size.height - LINE_SPACING;
2268 [rgroup setFrameOrigin:rect.origin];
2270 // now cheat on the size of the matrix: allow it to overlap (underlap)
2273 rect.size = [matrix cellSize];
2274 rect.size.width = 300;
2275 [matrix setCellSize:rect.size];
2276 [matrix sizeToCells];
2278 // Cheat on the position of the stuff on the right (the rgroup).
2279 // GAAAH, this code is such crap!
2280 rect = [rgroup frame];
2282 [rgroup setFrame:rect];
2285 rect.size.width = rect.size.height = 0;
2286 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2287 [box setTitlePosition:NSAtTop];
2288 [box setBorderType:NSBezelBorder];
2289 [box setTitle:@"Display Text"];
2291 rect.size.width = rect.size.height = 12;
2292 [box setContentViewMargins:rect.size];
2293 [box setContentView:group];
2296 [self placeChild:box on:parent];
2298 # endif // !USE_IPHONE
2302 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2305 [x] Grab desktop images
2306 [ ] Choose random image:
2307 [__________________________] [Choose]
2309 <boolean id="grabDesktopImages" _label="Grab desktop images"
2310 arg-unset="-no-grab-desktop"/>
2311 <boolean id="chooseRandomImages" _label="Grab desktop images"
2312 arg-unset="-choose-random-images"/>
2313 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2316 NSXMLElement *node2;
2319 # define SCREENS "Grab desktop images"
2320 # define PHOTOS "Choose random images"
2322 # define SCREENS "Grab screenshots"
2323 # define PHOTOS "Use photo library"
2326 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2327 [node2 setAttributesAsDictionary:
2328 @{ @"id": @"grabDesktopImages",
2329 @"_label": @ SCREENS,
2330 @"arg-unset": @"-no-grab-desktop",
2332 [self makeCheckbox:node2 on:parent];
2334 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2335 [node2 setAttributesAsDictionary:
2336 @{ @"id": @"chooseRandomImages",
2337 @"_label": @ PHOTOS,
2338 @"arg-set": @"-choose-random-images",
2340 [self makeCheckbox:node2 on:parent];
2342 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2343 [node2 setAttributesAsDictionary:
2344 @{ @"id": @"imageDirectory",
2345 @"_label": @"Images from:",
2346 @"arg": @"-image-directory %",
2348 [self makeFileSelector:node2 on:parent
2349 dirsOnly:YES withLabel:YES editable:YES];
2355 // Add a second, explanatory label below the file/URL selector.
2358 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2359 [self placeChild:lab2 on:parent];
2361 // Pack it in a little tighter vertically.
2362 NSRect r2 = [lab2 frame];
2365 [lab2 setFrameOrigin:r2.origin];
2367 # endif // USE_IPHONE
2371 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2375 [x] Check for Updates [ Monthly ]
2378 <boolean id="automaticallyChecksForUpdates"
2379 _label="Automatically check for updates"
2380 arg-unset="-no-automaticallyChecksForUpdates" />
2381 <select id="updateCheckInterval">
2382 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2383 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/>
2384 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2385 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2393 rect.size.width = rect.size.height = 1;
2394 rect.origin.x = rect.origin.y = 0;
2395 NSView *group = [[NSView alloc] initWithFrame:rect];
2397 NSXMLElement *node2;
2401 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2402 [node2 setAttributesAsDictionary:
2403 @{ @"id": @SUSUEnableAutomaticChecksKey,
2404 @"_label": @"Automatically check for updates",
2405 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2407 [self makeCheckbox:node2 on:group];
2411 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2412 [node2 setAttributesAsDictionary:
2413 @{ @"id": @SUScheduledCheckIntervalKey }];
2417 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2418 [node3 setAttributesAsDictionary:
2419 @{ @"id": @"hourly",
2420 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2421 @"_label": @"Hourly" }];
2422 [node3 setParent: node2];
2425 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2426 [node3 setAttributesAsDictionary:
2428 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2429 @"_label": @"Daily" }];
2430 [node3 setParent: node2];
2433 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2434 [node3 setAttributesAsDictionary:
2435 @{ @"id": @"weekly",
2436 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2437 @"_label": @"Weekly",
2439 [node3 setParent: node2];
2442 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2443 [node3 setAttributesAsDictionary:
2444 @{ @"id": @"monthly",
2445 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2446 @"_label": @"Monthly",
2448 [node3 setParent: node2];
2452 [self makeOptionMenu:node2 on:group];
2455 layout_group (group, TRUE);
2457 rect.size.width = rect.size.height = 0;
2458 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2459 [box setTitlePosition:NSNoTitle];
2460 [box setBorderType:NSNoBorder];
2461 [box setContentViewMargins:rect.size];
2462 [box setContentView:group];
2465 [self placeChild:box on:parent];
2470 # endif // !USE_IPHONE
2474 #pragma mark Layout for controls
2479 last_child (NSView *parent)
2481 NSArray *kids = [parent subviews];
2482 int nkids = [kids count];
2486 return [kids objectAtIndex:nkids-1];
2488 #endif // USE_IPHONE
2491 /* Add the child as a subview of the parent, positioning it immediately
2492 below or to the right of the previously-added child of that view.
2494 - (void) placeChild:
2500 on:(NSView *)parent right:(BOOL)right_p
2503 NSRect rect = [child frame];
2504 NSView *last = last_child (parent);
2506 rect.origin.x = LEFT_MARGIN;
2507 rect.origin.y = ([parent frame].size.height - rect.size.height
2509 } else if (right_p) {
2510 rect = [last frame];
2511 rect.origin.x += rect.size.width + COLUMN_SPACING;
2513 rect = [last frame];
2514 rect.origin.x = LEFT_MARGIN;
2515 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2517 NSRect r = [child frame];
2518 r.origin = rect.origin;
2520 [parent addSubview:child];
2522 # else // USE_IPHONE
2524 /* Controls is an array of arrays of the controls, divided into sections.
2525 Each hgroup / vgroup gets a nested array, too, e.g.:
2527 [ [ [ <label>, <checkbox> ],
2528 [ <label>, <checkbox> ],
2529 [ <label>, <checkbox> ] ],
2530 [ <label>, <text-field> ],
2531 [ <label>, <low-label>, <slider>, <high-label> ],
2532 [ <low-label>, <slider>, <high-label> ],
2536 If an element begins with a label, it is terminal, otherwise it is a
2537 group. There are (currently) never more than 4 elements in a single
2540 A blank vertical spacer is placed between each hgroup / vgroup,
2541 by making each of those a new section in the TableView.
2544 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2545 if ([controls count] == 0)
2546 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2547 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2549 if (!right_p || [current count] == 0) {
2550 // Nothing on the current line. Add this object.
2551 [current addObject: child];
2553 // Something's on the current line already.
2554 NSObject *old = [current objectAtIndex:[current count]-1];
2555 if ([old isKindOfClass:[NSMutableArray class]]) {
2556 // Already an array in this cell. Append.
2557 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2558 [(NSMutableArray *) old addObject: child];
2560 // Replace the control in this cell with an array, then append
2561 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2562 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2565 # endif // USE_IPHONE
2569 - (void) placeChild:(NSView *)child on:(NSView *)parent
2571 [self placeChild:child on:parent right:NO];
2577 // Start putting subsequent children in a new group, to create a new
2578 // section on the UITableView.
2580 - (void) placeSeparator
2582 if (! controls) return;
2583 if ([controls count] == 0) return;
2584 if ([[controls objectAtIndex:[controls count]-1]
2586 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2588 #endif // USE_IPHONE
2592 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2593 wrapped in <hgroup> or <vgroup> in the XML.
2595 - (void) makeGroup:(NSXMLNode *)node
2597 horizontal:(BOOL) horiz_p
2600 if (!horiz_p) [self placeSeparator];
2601 [self traverseChildren:node on:parent];
2602 if (!horiz_p) [self placeSeparator];
2603 # else // !USE_IPHONE
2605 rect.size.width = rect.size.height = 1;
2606 rect.origin.x = rect.origin.y = 0;
2607 NSView *group = [[NSView alloc] initWithFrame:rect];
2608 [self traverseChildren:node on:group];
2610 layout_group (group, horiz_p);
2612 rect.size.width = rect.size.height = 0;
2613 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2614 [box setTitlePosition:NSNoTitle];
2615 [box setBorderType:NSNoBorder];
2616 [box setContentViewMargins:rect.size];
2617 [box setContentView:group];
2620 [self placeChild:box on:parent];
2621 # endif // !USE_IPHONE
2627 layout_group (NSView *group, BOOL horiz_p)
2629 NSArray *kids = [group subviews];
2630 int nkids = [kids count];
2632 double maxx = 0, miny = 0;
2633 for (i = 0; i < nkids; i++) {
2634 NSView *kid = [kids objectAtIndex:i];
2635 NSRect r = [kid frame];
2638 maxx += r.size.width + COLUMN_SPACING;
2639 if (r.size.height > -miny) miny = -r.size.height;
2641 if (r.size.width > maxx) maxx = r.size.width;
2642 miny = r.origin.y - r.size.height;
2649 rect.size.width = maxx;
2650 rect.size.height = -miny;
2651 [group setFrame:rect];
2654 for (i = 0; i < nkids; i++) {
2655 NSView *kid = [kids objectAtIndex:i];
2656 NSRect r = [kid frame];
2658 r.origin.y = rect.size.height - r.size.height;
2660 x += r.size.width + COLUMN_SPACING;
2667 #endif // !USE_IPHONE
2670 /* Create some kind of control corresponding to the given XML node.
2672 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2674 NSString *name = [node name];
2676 if ([node kind] == NSXMLCommentKind)
2679 if ([node kind] == NSXMLTextKind) {
2680 NSString *s = [(NSString *) [node objectValue]
2681 stringByTrimmingCharactersInSet:
2682 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2683 if (! [s isEqualToString:@""]) {
2684 NSAssert1 (0, @"unexpected text: %@", s);
2689 if ([node kind] != NSXMLElementKind) {
2690 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2694 if ([name isEqualToString:@"hgroup"] ||
2695 [name isEqualToString:@"vgroup"]) {
2697 [self makeGroup:node on:parent
2698 horizontal:[name isEqualToString:@"hgroup"]];
2700 } else if ([name isEqualToString:@"command"]) {
2701 // do nothing: this is the "-root" business
2703 } else if ([name isEqualToString:@"video"]) {
2706 } else if ([name isEqualToString:@"boolean"]) {
2707 [self makeCheckbox:node on:parent];
2709 } else if ([name isEqualToString:@"string"]) {
2710 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2712 } else if ([name isEqualToString:@"file"]) {
2713 [self makeFileSelector:node on:parent
2714 dirsOnly:NO withLabel:YES editable:NO];
2716 } else if ([name isEqualToString:@"number"]) {
2717 [self makeNumberSelector:node on:parent];
2719 } else if ([name isEqualToString:@"select"]) {
2720 [self makeOptionMenu:node on:parent];
2722 } else if ([name isEqualToString:@"_description"]) {
2723 [self makeDescLabel:node on:parent];
2725 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2726 [self makeTextLoaderControlBox:node on:parent];
2728 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2729 [self makeImageLoaderControlBox:node on:parent];
2731 } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2732 [self makeUpdaterControlBox:node on:parent];
2735 NSAssert1 (0, @"unknown tag: %@", name);
2740 /* Iterate over and process the children of this XML node.
2742 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2744 NSArray *children = [node children];
2745 NSUInteger i, count = [children count];
2746 for (i = 0; i < count; i++) {
2747 NSXMLNode *child = [children objectAtIndex:i];
2748 [self makeControl:child on:parent];
2755 /* Kludgey magic to make the window enclose the controls we created.
2758 fix_contentview_size (NSView *parent)
2761 NSArray *kids = [parent subviews];
2762 int nkids = [kids count];
2763 NSView *text = 0; // the NSText at the bottom of the window
2764 double maxx = 0, miny = 0;
2767 /* Find the size of the rectangle taken up by each of the children
2768 except the final "NSText" child.
2770 for (i = 0; i < nkids; i++) {
2771 NSView *kid = [kids objectAtIndex:i];
2772 if ([kid isKindOfClass:[NSText class]]) {
2777 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2778 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2779 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2780 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2781 // f.origin.y + f.size.height, [kid class]);
2784 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2786 /* Now that we know the width of the window, set the width of the NSText to
2787 that, so that it can decide what its height needs to be.
2789 if (! text) abort();
2791 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2792 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2793 // f.origin.y + f.size.height, [text class]);
2795 // set the NSText's width (this changes its height).
2796 f.size.width = maxx - LEFT_MARGIN;
2799 // position the NSText below the last child (this gives us a new miny).
2801 f.origin.y = miny - f.size.height - LINE_SPACING;
2802 miny = f.origin.y - LINE_SPACING;
2805 // Lock the width of the field and unlock the height, and let it resize
2806 // once more, to compute the proper height of the text for that width.
2808 [(NSText *) text setHorizontallyResizable:NO];
2809 [(NSText *) text setVerticallyResizable:YES];
2810 [(NSText *) text sizeToFit];
2812 // Now lock the height too: no more resizing this text field.
2814 [(NSText *) text setVerticallyResizable:NO];
2816 // Now reposition the top edge of the text field to be back where it
2817 // was before we changed the height.
2819 float oh = f.size.height;
2821 float dh = f.size.height - oh;
2824 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2825 // If we do this in 10.6, the text field moves down, off the window.
2826 // So instead we repair it at the end, at the "WTF2" comment.
2829 // Also adjust the parent height by the change in height of the text field.
2832 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2833 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2834 // f.origin.y + f.size.height, [text class]);
2837 /* Set the contentView to the size of the children.
2840 // float yoff = f.size.height;
2841 f.size.width = maxx + LEFT_MARGIN;
2842 f.size.height = -(miny - LEFT_MARGIN*2);
2843 // yoff = f.size.height - yoff;
2844 [parent setFrame:f];
2846 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2847 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2849 /* Now move all of the kids up into the window.
2852 float shift = f.size.height;
2853 // NSLog(@"shift: %3.0f", shift);
2854 for (i = 0; i < nkids; i++) {
2855 NSView *kid = [kids objectAtIndex:i];
2857 f.origin.y += shift;
2859 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2860 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2861 // f.origin.y + f.size.height, [kid class]);
2866 parent: 420 x 541 @ 0 0
2867 text: 380 x 100 @ 20 22 miny=-501
2870 parent: 420 x 541 @ 0 0
2871 text: 380 x 100 @ 20 50 miny=-501
2874 // #### WTF2: See "WTF" above. If the text field is off the screen,
2875 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2878 if (f.origin.y < 50) { // magic numbers, yay
2883 /* Set the kids to track the top left corner of the window when resized.
2884 Set the NSText to track the bottom right corner as well.
2886 for (i = 0; i < nkids; i++) {
2887 NSView *kid = [kids objectAtIndex:i];
2888 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2889 if ([kid isKindOfClass:[NSText class]])
2890 mask |= NSViewWidthSizable|NSViewHeightSizable;
2891 [kid setAutoresizingMask:mask];
2894 # endif // !USE_IPHONE
2900 wrap_with_buttons (NSWindow *window, NSView *panel)
2904 // Make a box to hold the buttons at the bottom of the window.
2906 rect = [panel frame];
2907 rect.origin.x = rect.origin.y = 0;
2908 rect.size.height = 10;
2909 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2910 [bbox setTitlePosition:NSNoTitle];
2911 [bbox setBorderType:NSNoBorder];
2913 // Make some buttons: Default, Cancel, OK
2915 rect.origin.x = rect.origin.y = 0;
2916 rect.size.width = rect.size.height = 10;
2917 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2918 [reset setTitle:@"Reset to Defaults"];
2919 [reset setBezelStyle:NSRoundedBezelStyle];
2922 rect = [reset frame];
2923 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2924 [ok setTitle:@"OK"];
2925 [ok setBezelStyle:NSRoundedBezelStyle];
2927 rect = [bbox frame];
2928 rect.origin.x = rect.size.width - [ok frame].size.width;
2929 [ok setFrameOrigin:rect.origin];
2932 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2933 [cancel setTitle:@"Cancel"];
2934 [cancel setBezelStyle:NSRoundedBezelStyle];
2936 rect.origin.x -= [cancel frame].size.width + 10;
2937 [cancel setFrameOrigin:rect.origin];
2939 // Bind OK to RET and Cancel to ESC.
2940 [ok setKeyEquivalent:@"\r"];
2941 [cancel setKeyEquivalent:@"\e"];
2943 // The correct width for OK and Cancel buttons is 68 pixels
2944 // ("Human Interface Guidelines: Controls: Buttons:
2945 // Push Button Specifications").
2948 rect.size.width = 68;
2951 rect = [cancel frame];
2952 rect.size.width = 68;
2953 [cancel setFrame:rect];
2955 // It puts the buttons in the box or else it gets the hose again
2957 [bbox addSubview:ok];
2958 [bbox addSubview:cancel];
2959 [bbox addSubview:reset];
2962 // make a box to hold the button-box, and the preferences view
2964 rect = [bbox frame];
2965 rect.origin.y += rect.size.height;
2966 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
2967 [pbox setTitlePosition:NSNoTitle];
2968 [pbox setBorderType:NSBezelBorder];
2970 // Enforce a max height on the dialog, so that it's obvious to me
2971 // (on a big screen) when the dialog will fall off the bottom of
2972 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
2974 NSRect f = [panel frame];
2975 int screen_height = (768 // shortest "modern" Mac display
2977 - 56 // System Preferences toolbar
2978 - 140 // default magnified bottom dock icon
2980 if (f.size.height > screen_height) {
2981 NSLog(@"%@ height was %.0f; clipping to %d",
2982 [panel class], f.size.height, screen_height);
2983 f.size.height = screen_height;
2988 [pbox addSubview:panel];
2989 [pbox addSubview:bbox];
2992 [reset setAutoresizingMask:NSViewMaxXMargin];
2993 [cancel setAutoresizingMask:NSViewMinXMargin];
2994 [ok setAutoresizingMask:NSViewMinXMargin];
2995 [bbox setAutoresizingMask:NSViewWidthSizable];
2999 [ok setTarget:window];
3000 [cancel setTarget:window];
3001 [reset setTarget:window];
3002 [ok setAction:@selector(okAction:)];
3003 [cancel setAction:@selector(cancelAction:)];
3004 [reset setAction:@selector(resetAction:)];
3010 #endif // !USE_IPHONE
3013 /* Iterate over and process the children of the root node of the XML document.
3015 - (void)traverseTree
3018 NSView *parent = [self view];
3020 NSWindow *parent = self;
3022 NSXMLNode *node = xml_root;
3024 if (![[node name] isEqualToString:@"screensaver"]) {
3025 NSAssert (0, @"top level node is not <xscreensaver>");
3028 saver_name = [self parseXScreenSaverTag: node];
3029 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
3031 [saver_name retain];
3036 rect.origin.x = rect.origin.y = 0;
3037 rect.size.width = rect.size.height = 1;
3039 NSView *panel = [[NSView alloc] initWithFrame:rect];
3040 [self traverseChildren:node on:panel];
3041 fix_contentview_size (panel);
3043 NSView *root = wrap_with_buttons (parent, panel);
3044 [userDefaultsController setAppliesImmediately:NO];
3045 [globalDefaultsController setAppliesImmediately:NO];
3047 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
3049 rect = [parent frameRectForContentRect:[root frame]];
3050 [parent setFrame:rect display:NO];
3051 [parent setMinSize:rect.size];
3053 [parent setContentView:root];
3058 # else // USE_IPHONE
3060 CGRect r = [parent frame];
3061 r.size = [[UIScreen mainScreen] bounds].size;
3062 [parent setFrame:r];
3063 [self traverseChildren:node on:parent];
3065 # endif // USE_IPHONE
3069 - (void)parser:(NSXMLParser *)parser
3070 didStartElement:(NSString *)elt
3071 namespaceURI:(NSString *)ns
3072 qualifiedName:(NSString *)qn
3073 attributes:(NSDictionary *)attrs
3075 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3076 [e setKind:SimpleXMLElementKind];
3077 [e setAttributesAsDictionary:attrs];
3078 NSXMLElement *p = xml_parsing;
3082 xml_root = xml_parsing;
3085 - (void)parser:(NSXMLParser *)parser
3086 didEndElement:(NSString *)elt
3087 namespaceURI:(NSString *)ns
3088 qualifiedName:(NSString *)qn
3090 NSXMLElement *p = xml_parsing;
3092 NSLog(@"extra close: %@", elt);
3093 } else if (![[p name] isEqualToString:elt]) {
3094 NSLog(@"%@ closed by %@", [p name], elt);
3096 NSXMLElement *n = xml_parsing;
3097 xml_parsing = [n parent];
3102 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3104 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3105 [e setKind:SimpleXMLTextKind];
3106 NSXMLElement *p = xml_parsing;
3108 [e setObjectValue: string];
3113 # ifdef USE_PICKER_VIEW
3115 #pragma mark UIPickerView delegate methods
3117 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3119 return 1; // Columns
3122 - (NSInteger)pickerView:(UIPickerView *)pv
3123 numberOfRowsInComponent:(NSInteger)column
3125 NSAssert (column == 0, @"weird column");
3126 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3127 if (! a) return 0; // Too early?
3131 - (CGFloat)pickerView:(UIPickerView *)pv
3132 rowHeightForComponent:(NSInteger)column
3137 - (CGFloat)pickerView:(UIPickerView *)pv
3138 widthForComponent:(NSInteger)column
3140 NSAssert (column == 0, @"weird column");
3141 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3142 if (! a) return 0; // Too early?
3144 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3146 for (NSArray *a2 in a) {
3147 NSString *s = [a2 objectAtIndex:0];
3148 CGSize r = [s sizeWithFont:f];
3149 if (r.width > max) max = r.width;
3152 max *= 1.7; // WTF!!
3164 - (NSString *)pickerView:(UIPickerView *)pv
3165 titleForRow:(NSInteger)row
3166 forComponent:(NSInteger)column
3168 NSAssert (column == 0, @"weird column");
3169 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3170 if (! a) return 0; // Too early?
3171 a = [a objectAtIndex:row];
3172 NSAssert (a, @"internal error");
3173 return [a objectAtIndex:0];
3176 # endif // USE_PICKER_VIEW
3179 #pragma mark UITableView delegate methods
3181 - (void) addResetButton
3183 [[self navigationItem]
3184 setRightBarButtonItem: [[UIBarButtonItem alloc]
3185 initWithTitle: @"Reset to Defaults"
3186 style: UIBarButtonItemStyleBordered
3188 action:@selector(resetAction:)]];
3189 NSString *s = saver_name;
3190 if ([self view].frame.size.width > 320)
3191 s = [s stringByAppendingString: @" Settings"];
3192 [self navigationItem].title = s;
3196 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3201 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3202 // Number of vertically-stacked white boxes.
3203 return [controls count];
3206 - (NSInteger)tableView:(UITableView *)tableView
3207 numberOfRowsInSection:(NSInteger)section
3209 // Number of lines in each vertically-stacked white box.
3210 NSAssert (controls, @"internal error");
3211 return [[controls objectAtIndex:section] count];
3214 - (NSString *)tableView:(UITableView *)tv
3215 titleForHeaderInSection:(NSInteger)section
3217 // Titles above each vertically-stacked white box.
3218 // if (section == 0)
3219 // return [saver_name stringByAppendingString:@" Settings"];
3224 - (CGFloat)tableView:(UITableView *)tv
3225 heightForRowAtIndexPath:(NSIndexPath *)ip
3229 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3230 objectAtIndex:[ip indexAtPosition:1]];
3232 if ([ctl isKindOfClass:[NSArray class]]) {
3233 NSArray *set = (NSArray *) ctl;
3234 switch ([set count]) {
3235 case 4: // label + left/slider/right.
3236 case 3: // left/slider/right.
3237 h = FONT_SIZE * 3.0;
3239 case 2: // Checkboxes, or text fields.
3240 h = FONT_SIZE * 2.4;
3243 } else if ([ctl isKindOfClass:[UILabel class]]) {
3244 // Radio buttons in a multi-select list.
3245 h = FONT_SIZE * 1.9;
3247 # ifdef USE_HTML_LABELS
3248 } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3250 HTMLLabel *t = (HTMLLabel *) ctl;
3252 r.size.width = [tv frame].size.width;
3253 r.size.width -= LEFT_MARGIN * 2;
3258 # endif // USE_HTML_LABELS
3260 } else { // Does this ever happen?
3261 h = FONT_SIZE + LINE_SPACING * 2;
3264 if (h <= 0) abort();
3269 - (void)refreshTableView
3271 UITableView *tv = (UITableView *) [self view];
3272 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3273 NSInteger rows = [self numberOfSectionsInTableView:tv];
3274 for (int i = 0; i < rows; i++) {
3275 NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3276 for (int j = 0; j < cols; j++) {
3280 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3285 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3288 // Default opacity looks bad.
3289 // #### Oh great, this only works *sometimes*.
3290 UIView *v = [[self navigationItem] titleView];
3291 [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
3295 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3297 [NSTimer scheduledTimerWithTimeInterval: 0
3299 selector:@selector(refreshTableView)
3305 #ifndef USE_PICKER_VIEW
3307 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3308 button:(RadioButton *)b
3310 NSArray *item = [[b items] objectAtIndex: [b index]];
3311 NSString *pref_key = [item objectAtIndex:1];
3312 NSObject *pref_val = [item objectAtIndex:2];
3314 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3316 // Convert them both to strings and compare those, so that
3317 // we don't get screwed by int 1 versus string "1".
3318 // Will boolean true/1 screw us here too?
3320 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3321 ? (NSString *) pref_val
3322 : [(NSNumber *) pref_val stringValue]);
3323 NSString *current_str = ([current isKindOfClass:[NSString class]]
3324 ? (NSString *) current
3325 : [(NSNumber *) current stringValue]);
3326 BOOL match_p = [current_str isEqualToString:pref_str];
3328 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3331 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3333 [cell setAccessoryType:UITableViewCellAccessoryNone];
3337 - (void)tableView:(UITableView *)tv
3338 didSelectRowAtIndexPath:(NSIndexPath *)ip
3340 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3341 objectAtIndex:[ip indexAtPosition:1]];
3342 if (! [ctl isKindOfClass:[RadioButton class]])
3345 [self radioAction:ctl];
3346 [self refreshTableView];
3350 #endif // !USE_PICKER_VIEW
3354 - (UITableViewCell *)tableView:(UITableView *)tv
3355 cellForRowAtIndexPath:(NSIndexPath *)ip
3357 CGFloat ww = [tv frame].size.width;
3358 CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
3360 float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
3362 // Width of the column of labels on the left.
3363 CGFloat left_width = ww * 0.4;
3364 CGFloat right_edge = ww - LEFT_MARGIN;
3366 if (os_version < 7) // margins were wider on iOS 6.1
3369 CGFloat max = FONT_SIZE * 12;
3370 if (left_width > max) left_width = max;
3372 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3373 objectAtIndex:[ip indexAtPosition:1]];
3375 if ([ctl isKindOfClass:[NSArray class]]) {
3376 // This cell has a set of objects in it.
3377 NSArray *set = (NSArray *) ctl;
3378 switch ([set count]) {
3381 // With 2 elements, the first of the pair must be a label.
3382 UILabel *label = (UILabel *) [set objectAtIndex: 0];
3383 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3384 ctl = [set objectAtIndex: 1];
3386 CGRect r = [ctl frame];
3388 if ([ctl isKindOfClass:[UISwitch class]]) { // Checkboxes.
3389 r.size.width = 80; // Magic.
3390 r.origin.x = right_edge - r.size.width + 30; // beats me
3392 if (os_version < 7) // checkboxes were wider on iOS 6.1
3396 r.origin.x = left_width; // Text fields, etc.
3397 r.size.width = right_edge - r.origin.x;
3400 r.origin.y = (hh - r.size.height) / 2; // Center vertically.
3403 // Make a box and put the label and checkbox/slider into it.
3408 NSView *box = [[UIView alloc] initWithFrame:r];
3409 [box addSubview: ctl];
3411 // Let the label make use of any space not taken up by the control.
3413 r.origin.x = LEFT_MARGIN;
3415 r.size.width = [ctl frame].origin.x - r.origin.x;
3418 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3419 [box addSubview: label];
3427 // With 3 elements, 1 and 3 are labels.
3428 // With 4 elements, 1, 2 and 4 are labels.
3430 UILabel *top = ([set count] == 4
3431 ? [set objectAtIndex: i++]
3433 UILabel *left = [set objectAtIndex: i++];
3434 NSView *mid = [set objectAtIndex: i++];
3435 UILabel *right = [set objectAtIndex: i++];
3436 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3437 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3438 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3439 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3441 // 3 elements: control at top of cell.
3442 // 4 elements: center the control vertically.
3443 CGRect r = [mid frame];
3444 r.size.height = 32; // Unchangable height of the slider thumb.
3446 // Center the slider between left_width and right_edge.
3447 # ifdef LABEL_ABOVE_SLIDER
3448 r.origin.x = LEFT_MARGIN;
3450 r.origin.x = left_width;
3452 r.origin.y = (hh - r.size.height) / 2;
3453 r.size.width = right_edge - r.origin.x;
3457 r.size = [[top text] sizeWithFont:[top font]
3459 CGSizeMake (ww - LEFT_MARGIN*2, 100000)
3460 lineBreakMode:[top lineBreakMode]];
3461 # ifdef LABEL_ABOVE_SLIDER
3462 // Top label goes above, flush center/top.
3463 r.origin.x = (ww - r.size.width) / 2;
3465 # else // !LABEL_ABOVE_SLIDER
3466 // Label goes on the left.
3467 r.origin.x = LEFT_MARGIN;
3469 r.size.width = left_width - LEFT_MARGIN;
3471 # endif // !LABEL_ABOVE_SLIDER
3475 // Left label goes under control, flush left/bottom.
3476 r.size = [[left text] sizeWithFont:[left font]
3478 CGSizeMake(ww - LEFT_MARGIN*2, 100000)
3479 lineBreakMode:[left lineBreakMode]];
3480 r.origin.x = [mid frame].origin.x;
3481 r.origin.y = hh - r.size.height - 4;
3484 // Right label goes under control, flush right/bottom.
3486 r.size = [[right text] sizeWithFont:[right font]
3488 CGSizeMake(ww - LEFT_MARGIN*2, 1000000)
3489 lineBreakMode:[right lineBreakMode]];
3490 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3492 r.origin.y = [left frame].origin.y;
3495 // Make a box and put the labels and slider into it.
3500 NSView *box = [[UIView alloc] initWithFrame:r];
3502 [box addSubview: top];
3503 [box addSubview: left];
3504 [box addSubview: right];
3505 [box addSubview: mid];
3511 NSAssert (0, @"unhandled size");
3513 } else { // A single view, not a pair.
3514 CGRect r = [ctl frame];
3515 r.origin.x = LEFT_MARGIN;
3517 r.size.width = right_edge - r.origin.x;
3522 NSString *id = @"Cell";
3523 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
3525 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3526 reuseIdentifier: id]
3529 for (UIView *subview in [cell.contentView subviews])
3530 [subview removeFromSuperview];
3531 [cell.contentView addSubview: ctl];
3532 CGRect r = [ctl frame];
3536 cell.selectionStyle = UITableViewCellSelectionStyleNone;
3537 [cell setAccessoryType:UITableViewCellAccessoryNone];
3539 # ifndef USE_PICKER_VIEW
3540 if ([ctl isKindOfClass:[RadioButton class]])
3541 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3542 # endif // USE_PICKER_VIEW
3546 # endif // USE_IPHONE
3549 /* When this object is instantiated, it parses the XML file and creates
3550 controls on itself that are hooked up to the appropriate preferences.
3551 The default size of the view is just big enough to hold them all.
3553 - (id)initWithXML: (NSData *) xml_data
3554 options: (const XrmOptionDescRec *) _opts
3555 controller: (NSUserDefaultsController *) _prefs
3556 globalController: (NSUserDefaultsController *) _globalPrefs
3557 defaults: (NSDictionary *) _defs
3560 self = [super init];
3561 # else // !USE_IPHONE
3562 self = [super initWithStyle:UITableViewStyleGrouped];
3563 self.title = [saver_name stringByAppendingString:@" Settings"];
3564 # endif // !USE_IPHONE
3565 if (! self) return 0;
3567 // instance variables
3569 defaultOptions = _defs;
3570 userDefaultsController = [_prefs retain];
3571 globalDefaultsController = [_globalPrefs retain];
3573 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3576 NSAssert1 (0, @"XML Error: %@",
3577 [[NSString alloc] initWithData:xml_data
3578 encoding:NSUTF8StringEncoding]);
3581 [xmlDoc setDelegate:self];
3582 if (! [xmlDoc parse]) {
3583 NSError *err = [xmlDoc parserError];
3584 NSAssert2 (0, @"XML Error: %@: %@",
3585 [[NSString alloc] initWithData:xml_data
3586 encoding:NSUTF8StringEncoding],
3591 [self traverseTree];
3595 [self addResetButton];
3604 [saver_name release];
3605 [userDefaultsController release];
3606 [globalDefaultsController release];
3609 [pref_keys release];
3610 [pref_ctls release];
3611 # ifdef USE_PICKER_VIEW
3612 [picker_values release];