1 /* xscreensaver, Copyright (c) 2006-2016 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];
136 - (SimpleXMLNode *) parent { return parent; }
138 - (void) setParent:(SimpleXMLNode *)p
140 NSAssert (!parent, @"parent already set");
143 NSMutableArray *kids = [p children];
145 kids = [NSMutableArray arrayWithCapacity:10];
146 [p setChildren:kids];
148 [kids addObject:self];
153 #pragma mark textMode value transformer
155 // A value transformer for mapping "url" to "3" and vice versa in the
156 // "textMode" preference, since NSMatrix uses NSInteger selectedIndex.
159 @interface TextModeTransformer: NSValueTransformer {}
161 @implementation TextModeTransformer
162 + (Class)transformedValueClass { return [NSString class]; }
163 + (BOOL)allowsReverseTransformation { return YES; }
165 - (id)transformedValue:(id)value {
166 if ([value isKindOfClass:[NSString class]]) {
168 if ([value isEqualToString:@"date"]) { i = 0; }
169 else if ([value isEqualToString:@"literal"]) { i = 1; }
170 else if ([value isEqualToString:@"file"]) { i = 2; }
171 else if ([value isEqualToString:@"url"]) { i = 3; }
172 else if ([value isEqualToString:@"program"]) { i = 4; }
174 value = [NSNumber numberWithInt: i];
179 - (id)reverseTransformedValue:(id)value {
180 if ([value isKindOfClass:[NSNumber class]]) {
181 switch ((int) [value doubleValue]) {
182 case 0: value = @"date"; break;
183 case 1: value = @"literal"; break;
184 case 2: value = @"file"; break;
185 case 3: value = @"url"; break;
186 case 4: value = @"program"; break;
195 #pragma mark Implementing radio buttons
197 /* The UIPickerView is a hideous and uncustomizable piece of shit.
198 I can't believe Apple actually released that thing on the world.
199 Let's fake up some radio buttons instead.
202 #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
204 @interface RadioButton : UILabel
210 @property(nonatomic) int index;
211 @property(nonatomic, retain) NSArray *items;
215 @implementation RadioButton
220 - (id) initWithIndex:(int)_index items:_items
222 self = [super initWithFrame:CGRectZero];
224 items = [_items retain];
226 [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
227 [self setBackgroundColor:[UIColor clearColor]];
236 # endif // !USE_PICKER_VIEW
239 # pragma mark Implementing labels with clickable links
241 #if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
243 @interface HTMLLabel : UIView <UIWebViewDelegate>
250 @property(nonatomic, retain) NSString *html;
251 @property(nonatomic, retain) UIWebView *webView;
253 - (id) initWithHTML:(NSString *)h font:(UIFont *)f;
254 - (id) initWithText:(NSString *)t font:(UIFont *)f;
255 - (void) setHTML:(NSString *)h;
256 - (void) setText:(NSString *)t;
261 @implementation HTMLLabel
266 - (id) initWithHTML:(NSString *)h font:(UIFont *)f
269 if (! self) return 0;
271 webView = [[UIWebView alloc] init];
272 webView.delegate = self;
273 webView.dataDetectorTypes = UIDataDetectorTypeNone;
274 self. autoresizingMask = UIViewAutoresizingNone; // we do it manually
275 webView.autoresizingMask = UIViewAutoresizingNone;
276 webView.scrollView.scrollEnabled = NO;
277 webView.scrollView.bounces = NO;
279 [webView setBackgroundColor:[UIColor clearColor]];
281 [self addSubview: webView];
286 - (id) initWithText:(NSString *)t font:(UIFont *)f
288 self = [self initWithHTML:@"" font:f];
289 if (! self) return 0;
295 - (void) setHTML: (NSString *)h
299 if (html) [html release];
302 [NSString stringWithFormat:
303 @"<!DOCTYPE HTML PUBLIC "
304 "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
305 " \"http://www.w3.org/TR/html4/loose.dtd\">"
308 // "<META NAME=\"viewport\" CONTENT=\""
309 // "width=device-width"
310 // "initial-scale=1.0;"
311 // "maximum-scale=1.0;\">"
315 " margin: 0; padding: 0; border: 0;"
316 " font-family: \"%@\";"
317 " font-size: %.4fpx;" // Must be "px", not "pt"!
318 " line-height: %.4fpx;" // And no spaces before it.
319 " -webkit-text-size-adjust: none;"
332 [webView stopLoading];
333 [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
337 static char *anchorize (const char *url);
339 - (void) setText: (NSString *)t
341 t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
342 whitespaceCharacterSet]];
343 t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
344 t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
345 t = [t stringByReplacingOccurrencesOfString:@">" withString:@">"];
346 t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
347 t = [t stringByReplacingOccurrencesOfString:@"<P> "
348 withString:@"<P> "];
349 t = [t stringByReplacingOccurrencesOfString:@"\n "
350 withString:@"<BR> "];
354 [t componentsSeparatedByCharactersInSet:
355 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
356 if ([s hasPrefix:@"http://"] ||
357 [s hasPrefix:@"https://"]) {
358 char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
359 NSString *a2 = [NSString stringWithCString: anchor
360 encoding: NSUTF8StringEncoding];
361 s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
364 h = [NSString stringWithFormat: @"%@ %@", h, s];
367 h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"];
368 h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"];
369 h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
370 whitespaceAndNewlineCharacterSet]];
376 -(BOOL) webView:(UIWebView *)wv
377 shouldStartLoadWithRequest:(NSURLRequest *)req
378 navigationType:(UIWebViewNavigationType)type
380 // Force clicked links to open in Safari, not in this window.
381 if (type == UIWebViewNavigationTypeLinkClicked) {
382 [[UIApplication sharedApplication] openURL:[req URL]];
389 - (void) setFrame: (CGRect)r
394 [webView setFrame: r];
398 - (NSString *) stripTags:(NSString *)str
400 NSString *result = @"";
403 str = [str stringByReplacingOccurrencesOfString:@"<P>"
404 withString:@"<BR><BR>"
405 options:NSCaseInsensitiveSearch
406 range:NSMakeRange(0, [str length])];
407 str = [str stringByReplacingOccurrencesOfString:@"<BR>"
409 options:NSCaseInsensitiveSearch
410 range:NSMakeRange(0, [str length])];
413 for (NSString *s in [str componentsSeparatedByString: @"<"]) {
414 NSRange r = [s rangeOfString:@">"];
416 s = [s substringFromIndex: r.location + r.length];
417 result = [result stringByAppendingString: s];
420 // Compress internal horizontal whitespace.
423 for (NSString *s in [str componentsSeparatedByCharactersInSet:
424 [NSCharacterSet whitespaceCharacterSet]]) {
425 if ([result length] == 0)
427 else if ([s length] > 0)
428 result = [NSString stringWithFormat: @"%@ %@", result, s];
437 CGRect r = [self frame];
439 /* It would be sensible to just ask the UIWebView how tall the page is,
440 instead of hoping that NSString and UIWebView measure fonts and do
441 wrapping in exactly the same way, but since UIWebView is asynchronous,
442 we'd have to wait for the document to load first, e.g.:
444 - Start the document loading;
445 - return a default height to use for the UITableViewCell;
446 - wait for the webViewDidFinishLoad delegate method to fire;
447 - then force the UITableView to reload, to pick up the new height.
449 But I couldn't make that work.
452 r.size.height = [[webView
453 stringByEvaluatingJavaScriptFromString:
454 @"document.body.offsetHeight"]
457 NSString *text = [self stripTags: html];
460 s = [text sizeWithFont: font
462 lineBreakMode:NSLineBreakByWordWrapping];
463 r.size.height = s.height;
480 #endif // USE_IPHONE && USE_HTML_LABELS
483 @interface XScreenSaverConfigSheet (Private)
485 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
488 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
489 - (void) placeChild: (NSView *)c on:(NSView *)p;
490 static NSView *last_child (NSView *parent);
491 static void layout_group (NSView *group, BOOL horiz_p);
493 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
494 - (void) placeChild: (NSObject *)c on:(NSView *)p;
495 - (void) placeSeparator;
496 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
497 - (void) refreshTableView;
498 # endif // USE_IPHONE
503 @implementation XScreenSaverConfigSheet
505 # define LEFT_MARGIN 20 // left edge of window
506 # define COLUMN_SPACING 10 // gap between e.g. labels and text fields
507 # define LEFT_LABEL_WIDTH 70 // width of all left labels
508 # define LINE_SPACING 10 // leading between each line
510 # define FONT_SIZE 17 // Magic hardcoded UITableView font size.
512 #pragma mark Talking to the resource database
515 /* Normally we read resources by looking up "KEY" in the database
516 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
517 app, everything is stored in the database "org.jwz.xscreensaver"
518 instead, so transform keys to "SAVERNAME.KEY".
520 NOTE: This is duplicated in PrefsReader.m, cause I suck.
522 - (NSString *) makeKey:(NSString *)key
525 NSString *prefix = [saver_name stringByAppendingString:@"."];
526 if (! [key hasPrefix:prefix]) // Don't double up!
527 key = [prefix stringByAppendingString:key];
533 - (NSString *) makeCKey:(const char *)key
535 return [self makeKey:[NSString stringWithCString:key
536 encoding:NSUTF8StringEncoding]];
540 /* Given a command-line option, returns the corresponding resource name.
541 Any arguments in the switch string are ignored (e.g., "-foo x").
543 - (NSString *) switchToResource:(NSString *)cmdline_switch
544 opts:(const XrmOptionDescRec *)opts_array
545 valRet:(NSString **)val_ret
549 NSAssert(cmdline_switch, @"cmdline switch is null");
550 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
551 encoding:NSUTF8StringEncoding]) {
552 NSAssert1(0, @"unable to convert %@", cmdline_switch);
555 char *s = strpbrk(buf, " \t\r\n");
559 while (*tail && (*tail == ' ' || *tail == '\t'))
563 while (opts_array[0].option) {
564 if (!strcmp (opts_array[0].option, buf)) {
567 if (opts_array[0].argKind == XrmoptionNoArg) {
569 NSAssert1 (0, @"expected no args to switch: \"%@\"",
571 ret = opts_array[0].value;
574 NSAssert1 (0, @"expected args to switch: \"%@\"",
581 ? [NSString stringWithCString:ret
582 encoding:NSUTF8StringEncoding]
585 const char *res = opts_array[0].specifier;
586 while (*res && (*res == '.' || *res == '*'))
588 return [self makeCKey:res];
593 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
598 - (NSUserDefaultsController *)controllerForKey:(NSString *)key
600 static NSDictionary *a = 0;
602 a = UPDATER_DEFAULTS;
605 if ([a objectForKey:key])
606 // These preferences are global to all xscreensavers.
607 return globalDefaultsController;
609 // All other preferences are per-saver.
610 return userDefaultsController;
616 // Called when a slider is bonked.
618 - (void)sliderAction:(UISlider*)sender
620 if ([active_text_field canResignFirstResponder])
621 [active_text_field resignFirstResponder];
622 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
624 // Hacky API. See comment in InvertedSlider.m.
625 double v = ([sender isKindOfClass: [InvertedSlider class]]
626 ? [(InvertedSlider *) sender transformedValue]
629 [[self controllerForKey:pref_key]
630 setObject:((v == (int) v)
631 ? [NSNumber numberWithInt:(int) v]
632 : [NSNumber numberWithDouble: v])
636 // Called when a checkbox/switch is bonked.
638 - (void)switchAction:(UISwitch*)sender
640 if ([active_text_field canResignFirstResponder])
641 [active_text_field resignFirstResponder];
642 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
643 NSString *v = ([sender isOn] ? @"true" : @"false");
644 [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
647 # ifdef USE_PICKER_VIEW
648 // Called when a picker is bonked.
650 - (void)pickerView:(UIPickerView *)pv
651 didSelectRow:(NSInteger)row
652 inComponent:(NSInteger)column
654 if ([active_text_field canResignFirstResponder])
655 [active_text_field resignFirstResponder];
657 NSAssert (column == 0, @"internal error");
658 NSArray *a = [picker_values objectAtIndex: [pv tag]];
659 if (! a) return; // Too early?
660 a = [a objectAtIndex:row];
661 NSAssert (a, @"missing row");
663 //NSString *label = [a objectAtIndex:0];
664 NSString *pref_key = [a objectAtIndex:1];
665 NSObject *pref_val = [a objectAtIndex:2];
666 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
668 # else // !USE_PICKER_VIEW
670 // Called when a RadioButton is bonked.
672 - (void)radioAction:(RadioButton*)sender
674 if ([active_text_field canResignFirstResponder])
675 [active_text_field resignFirstResponder];
677 NSArray *item = [[sender items] objectAtIndex: [sender index]];
678 NSString *pref_key = [item objectAtIndex:1];
679 NSObject *pref_val = [item objectAtIndex:2];
680 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
683 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
685 active_text_field = tf;
689 - (void)textFieldDidEndEditing:(UITextField *)tf
691 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
692 NSString *txt = [tf text];
693 [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
696 - (BOOL)textFieldShouldReturn:(UITextField *)tf
698 active_text_field = nil;
699 [tf resignFirstResponder];
703 # endif // !USE_PICKER_VIEW
710 - (void) okAction:(NSObject *)arg
712 // Without the setAppliesImmediately:, when the saver restarts, it's still
713 // got the old settings. -[XScreenSaverConfigSheet traverseTree] sets this
714 // to NO; default is YES.
716 // #### However: I'm told that when these are set to YES, then changes to
717 // 'textLiteral', 'textURL' and 'textProgram' are ignored, but 'textFile'
718 // works. In StarWars, at least...
720 [userDefaultsController setAppliesImmediately:YES];
721 [globalDefaultsController setAppliesImmediately:YES];
722 [userDefaultsController commitEditing];
723 [globalDefaultsController commitEditing];
724 [userDefaultsController save:self];
725 [globalDefaultsController save:self];
726 [NSApp endSheet:self returnCode:NSOKButton];
730 - (void) cancelAction:(NSObject *)arg
732 [userDefaultsController revert:self];
733 [globalDefaultsController revert:self];
734 [NSApp endSheet:self returnCode:NSCancelButton];
737 # endif // !USE_IPHONE
740 - (void) resetAction:(NSObject *)arg
743 [userDefaultsController revertToInitialValues:self];
744 [globalDefaultsController revertToInitialValues:self];
747 for (NSString *key in defaultOptions) {
748 NSObject *val = [defaultOptions objectForKey:key];
749 [[self controllerForKey:key] setObject:val forKey:key];
752 for (UIControl *ctl in pref_ctls) {
753 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
754 [self bindResource:ctl key:pref_key reload:YES];
757 [self refreshTableView];
758 # endif // USE_IPHONE
762 /* Connects a control (checkbox, etc) to the corresponding preferences key.
764 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
765 reload:(BOOL)reload_p
767 NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
769 NSDictionary *opts_dict = nil;
770 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
772 : ([control isKindOfClass:[NSMatrix class]]
776 if ([control isKindOfClass:[NSMatrix class]]) {
777 opts_dict = @{ NSValueTransformerNameBindingOption:
778 @"TextModeTransformer" };
783 withKeyPath:[@"values." stringByAppendingString: pref_key]
788 NSObject *val = [prefs objectForKey:pref_key];
792 if ([val isKindOfClass:[NSString class]]) {
793 sval = (NSString *) val;
794 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
795 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
796 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
799 dval = [sval doubleValue];
800 } else if ([val isKindOfClass:[NSNumber class]]) {
801 // NSBoolean (__NSCFBoolean) is really NSNumber.
802 dval = [(NSNumber *) val doubleValue];
803 sval = [(NSNumber *) val stringValue];
806 if ([control isKindOfClass:[UISlider class]]) {
807 sel = @selector(sliderAction:);
808 // Hacky API. See comment in InvertedSlider.m.
809 if ([control isKindOfClass:[InvertedSlider class]])
810 [(InvertedSlider *) control setTransformedValue: dval];
812 [(UISlider *) control setValue: dval];
813 } else if ([control isKindOfClass:[UISwitch class]]) {
814 sel = @selector(switchAction:);
815 [(UISwitch *) control setOn: ((int) dval != 0)];
816 # ifdef USE_PICKER_VIEW
817 } else if ([control isKindOfClass:[UIPickerView class]]) {
819 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
821 # else // !USE_PICKER_VIEW
822 } else if ([control isKindOfClass:[RadioButton class]]) {
823 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
824 } else if ([control isKindOfClass:[UITextField class]]) {
826 [(UITextField *) control setText: sval];
827 # endif // !USE_PICKER_VIEW
829 NSAssert (0, @"unknown class");
832 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
836 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
837 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
840 [pref_keys addObject: [self makeKey:pref_key]];
841 [pref_ctls addObject: control];
842 ((UIControl *) control).tag = [pref_keys count] - 1;
845 [(UIControl *) control addTarget:self action:sel
846 forControlEvents:UIControlEventValueChanged];
850 # endif // USE_IPHONE
853 NSObject *def = [[prefs defaults] objectForKey:pref_key];
854 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
855 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
856 s = [NSString stringWithFormat:@"%@ = %@", s,
857 ([def isKindOfClass:[NSString class]]
858 ? [NSString stringWithFormat:@"\"%@\"", def]
860 s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0];
861 s = [NSString stringWithFormat:@"%@ %@ / %@", s,
862 [def class], [control class]];
864 s = [NSString stringWithFormat:@"%@ / %@", s, bindto];
871 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
873 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
878 - (void) bindSwitch:(NSObject *)control
879 cmdline:(NSString *)cmd
881 [self bindResource:control
882 key:[self switchToResource:cmd opts:opts valRet:0]];
886 #pragma mark Text-manipulating utilities
890 unwrap (NSString *text)
892 // Unwrap lines: delete \n but do not delete \n\n.
894 NSArray *lines = [text componentsSeparatedByString:@"\n"];
895 NSUInteger i, nlines = [lines count];
898 text = @"\n"; // start with one blank line
900 // skip trailing blank lines in file
901 for (i = nlines-1; i > 0; i--) {
902 NSString *s = (NSString *) [lines objectAtIndex:i];
908 // skip leading blank lines in file
909 for (i = 0; i < nlines; i++) {
910 NSString *s = (NSString *) [lines objectAtIndex:i];
917 for (; i < nlines; i++) {
918 NSString *s = (NSString *) [lines objectAtIndex:i];
919 if ([s length] == 0) {
920 text = [text stringByAppendingString:@"\n\n"];
922 } else if ([s characterAtIndex:0] == ' ' ||
923 [s hasPrefix:@"Copyright "] ||
924 [s hasPrefix:@"http://"]) {
925 // don't unwrap if the following line begins with whitespace,
926 // or with the word "Copyright", or if it begins with a URL.
928 text = [text stringByAppendingString:@"\n"];
929 text = [text stringByAppendingString:s];
934 text = [text stringByAppendingString:@" "];
935 text = [text stringByAppendingString:s];
946 /* Makes the text up to the first comma be bold.
949 boldify (NSText *nstext)
951 NSString *text = [nstext string];
952 NSRange r = [text rangeOfString:@"," options:0];
953 r.length = r.location+1;
957 NSFont *font = [nstext font];
958 font = [NSFont boldSystemFontOfSize:[font pointSize]];
959 [nstext setFont:font range:r];
961 # endif // !USE_IPHONE
964 /* Creates a human-readable anchor to put on a URL.
967 anchorize (const char *url)
969 const char *wiki = "http://en.wikipedia.org/wiki/";
970 const char *math = "http://mathworld.wolfram.com/";
971 if (!strncmp (wiki, url, strlen(wiki))) {
972 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
973 strcpy (anchor, "Wikipedia: \"");
974 const char *in = url + strlen(wiki);
975 char *out = anchor + strlen(anchor);
979 } else if (*in == '#') {
982 } else if (*in == '%') {
988 sscanf (hex, "%x", &n);
1000 } else if (!strncmp (math, url, strlen(math))) {
1001 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1002 strcpy (anchor, "MathWorld: \"");
1003 const char *start = url + strlen(wiki);
1004 const char *in = start;
1005 char *out = anchor + strlen(anchor);
1009 } else if (in != start && *in >= 'A' && *in <= 'Z') {
1012 } else if (!strncmp (in, ".htm", 4)) {
1024 return strdup (url);
1029 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
1031 /* Converts any http: URLs in the given text field to clickable links.
1034 hreffify (NSText *nstext)
1037 NSString *text = [nstext string];
1038 [nstext setRichText:YES];
1040 NSString *text = [nstext text];
1043 int L = [text length];
1044 NSRange start; // range is start-of-search to end-of-string
1047 while (start.location < L) {
1049 // Find the beginning of a URL...
1051 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1052 if (r2.location == NSNotFound)
1055 // Next time around, start searching after this.
1056 start.location = r2.location + r2.length;
1057 start.length = L - start.location;
1059 // Find the end of a URL (whitespace or EOF)...
1061 NSRange r3 = [text rangeOfCharacterFromSet:
1062 [NSCharacterSet whitespaceAndNewlineCharacterSet]
1063 options:0 range:start];
1064 if (r3.location == NSNotFound) // EOF
1065 r3.location = L, r3.length = 0;
1067 // Next time around, start searching after this.
1068 start.location = r3.location;
1069 start.length = L - start.location;
1071 // Set r2 to the start/length of this URL.
1072 r2.length = start.location - r2.location;
1075 NSString *nsurl = [text substringWithRange:r2];
1076 const char *url = [nsurl UTF8String];
1078 // If this is a Wikipedia URL, make the linked text be prettier.
1080 char *anchor = anchorize(url);
1084 // Construct the RTF corresponding to <A HREF="url">anchor</A>
1086 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1087 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1088 sprintf (rtf, fmt, url, anchor);
1090 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1091 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1093 # else // !USE_IPHONE
1094 // *anchor = 0; // Omit Wikipedia anchor
1095 text = [text stringByReplacingCharactersInRange:r2
1096 withString:[NSString stringWithCString:anchor
1097 encoding:NSUTF8StringEncoding]];
1098 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1099 // withString:@"\n\n"];
1100 # endif // !USE_IPHONE
1104 int L2 = [text length]; // might have changed
1105 start.location -= (L - L2);
1110 [nstext setText:text];
1115 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1119 #pragma mark Creating controls from XML
1122 /* Parse the attributes of an XML tag into a dictionary.
1123 For input, the dictionary should have as attributes the keys, each
1124 with @"" as their value.
1125 On output, the dictionary will set the keys to the values specified,
1126 and keys that were not specified will not be present in the dictionary.
1127 Warnings are printed if there are duplicate or unknown attributes.
1129 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1131 NSArray *attrs = [(NSXMLElement *) node attributes];
1132 NSUInteger n = [attrs count];
1135 // For each key in the dictionary, fill in the dict with the corresponding
1136 // value. The value @"" is assumed to mean "un-set". Issue a warning if
1137 // an attribute is specified twice.
1139 for (i = 0; i < n; i++) {
1140 NSXMLNode *attr = [attrs objectAtIndex:i];
1141 NSString *key = [attr name];
1142 NSString *val = [attr objectValue];
1143 NSString *old = [dict objectForKey:key];
1146 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1147 } else if ([old length] != 0) {
1148 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1150 [dict setValue:val forKey:key];
1154 // Remove from the dictionary any keys whose value is still @"",
1155 // meaning there was no such attribute specified.
1157 NSArray *keys = [dict allKeys];
1159 for (i = 0; i < n; i++) {
1160 NSString *key = [keys objectAtIndex:i];
1161 NSString *val = [dict objectForKey:key];
1162 if ([val length] == 0)
1163 [dict removeObjectForKey:key];
1167 // Kludge for starwars.xml:
1168 // If there is a "_low-label" and no "_label", but "_low-label" contains
1169 // spaces, divide them.
1170 NSString *lab = [dict objectForKey:@"_label"];
1171 NSString *low = [dict objectForKey:@"_low-label"];
1174 [[[low stringByTrimmingCharactersInSet:
1175 [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1176 componentsSeparatedByString: @" "]
1177 filteredArrayUsingPredicate:
1178 [NSPredicate predicateWithFormat:@"length > 0"]];
1179 if (split && [split count] == 2) {
1180 [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1181 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1184 # endif // USE_IPHONE
1188 /* Handle the options on the top level <xscreensaver> tag.
1190 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1192 NSMutableDictionary *dict = [@{ @"name": @"",
1196 [self parseAttrs:dict node:node];
1197 NSString *name = [dict objectForKey:@"name"];
1198 NSString *label = [dict objectForKey:@"_label"];
1202 NSAssert1 (label, @"no _label in %@", [node name]);
1203 NSAssert1 (name, @"no name in \"%@\"", label);
1208 /* Creates a label: an un-editable NSTextField displaying the given text.
1210 - (LABEL *) makeLabel:(NSString *)text
1213 rect.origin.x = rect.origin.y = 0;
1214 rect.size.width = rect.size.height = 10;
1216 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1217 [lab setSelectable:NO];
1218 [lab setEditable:NO];
1219 [lab setBezeled:NO];
1220 [lab setDrawsBackground:NO];
1221 [lab setStringValue:text];
1223 # else // USE_IPHONE
1224 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1225 [lab setText: [text stringByTrimmingCharactersInSet:
1226 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1227 [lab setBackgroundColor:[UIColor clearColor]];
1228 [lab setNumberOfLines:0]; // unlimited
1229 // [lab setLineBreakMode:UILineBreakModeWordWrap];
1230 [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1231 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1232 UIViewAutoresizingFlexibleHeight)];
1233 # endif // USE_IPHONE
1239 /* Creates the checkbox (NSButton) described by the given XML node.
1241 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1243 NSMutableDictionary *dict = [@{ @"id": @"",
1248 [self parseAttrs:dict node:node];
1249 NSString *label = [dict objectForKey:@"_label"];
1250 NSString *arg_set = [dict objectForKey:@"arg-set"];
1251 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1256 NSAssert1 (0, @"no _label in %@", [node name]);
1259 if (!arg_set && !arg_unset) {
1260 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
1263 if (arg_set && arg_unset) {
1264 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
1268 // sanity-check the choice of argument names.
1270 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1271 [arg_set hasPrefix:@"--no-"]))
1272 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1274 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1275 ![arg_unset hasPrefix:@"--no-"]))
1276 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1280 rect.origin.x = rect.origin.y = 0;
1281 rect.size.width = rect.size.height = 10;
1285 NSButton *button = [[NSButton alloc] initWithFrame:rect];
1286 [button setButtonType:NSSwitchButton];
1287 [button setTitle:label];
1289 [self placeChild:button on:parent];
1291 # else // USE_IPHONE
1293 LABEL *lab = [self makeLabel:label];
1294 [self placeChild:lab on:parent];
1295 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1296 [self placeChild:button on:parent right:YES];
1298 # endif // USE_IPHONE
1300 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1305 /* Creates the number selection control described by the given XML node.
1306 If "type=slider", it's an NSSlider.
1307 If "type=spinbutton", it's a text field with up/down arrows next to it.
1309 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1311 NSMutableDictionary *dict = [@{ @"id": @"",
1314 @"_high-label": @"",
1322 [self parseAttrs:dict node:node];
1323 NSString *label = [dict objectForKey:@"_label"];
1324 NSString *low_label = [dict objectForKey:@"_low-label"];
1325 NSString *high_label = [dict objectForKey:@"_high-label"];
1326 NSString *type = [dict objectForKey:@"type"];
1327 NSString *arg = [dict objectForKey:@"arg"];
1328 NSString *low = [dict objectForKey:@"low"];
1329 NSString *high = [dict objectForKey:@"high"];
1330 NSString *def = [dict objectForKey:@"default"];
1331 NSString *cvt = [dict objectForKey:@"convert"];
1335 NSAssert1 (arg, @"no arg in %@", label);
1336 NSAssert1 (type, @"no type in %@", label);
1339 NSAssert1 (0, @"no low in %@", [node name]);
1343 NSAssert1 (0, @"no high in %@", [node name]);
1347 NSAssert1 (0, @"no default in %@", [node name]);
1350 if (cvt && ![cvt isEqualToString:@"invert"]) {
1351 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1355 // If either the min or max field contains a decimal point, then this
1356 // option may have a floating point value; otherwise, it is constrained
1357 // to be an integer.
1359 NSCharacterSet *dot =
1360 [NSCharacterSet characterSetWithCharactersInString:@"."];
1361 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1362 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1364 if ([type isEqualToString:@"slider"]
1365 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1366 || [type isEqualToString:@"spinbutton"]
1371 rect.origin.x = rect.origin.y = 0;
1372 rect.size.width = 150;
1373 rect.size.height = 23; // apparent min height for slider with ticks...
1375 slider = [[InvertedSlider alloc] initWithFrame:rect
1377 integers: !float_p];
1378 [slider setMaxValue:[high doubleValue]];
1379 [slider setMinValue:[low doubleValue]];
1381 int range = [slider maxValue] - [slider minValue] + 1;
1384 while (range2 > max_ticks)
1388 // If we have elided ticks, leave it at the max number of ticks.
1389 if (range != range2 && range2 < max_ticks)
1392 // If it's a float, always display the max number of ticks.
1393 if (float_p && range2 < max_ticks)
1396 [slider setNumberOfTickMarks:range2];
1398 [slider setAllowsTickMarkValuesOnly:
1399 (range == range2 && // we are showing the actual number of ticks
1400 !float_p)]; // and we want integer results
1401 # endif // !USE_IPHONE
1403 // #### Note: when the slider's range is large enough that we aren't
1404 // showing all possible ticks, the slider's value is not constrained
1405 // to be an integer, even though it should be...
1406 // Maybe we need to use a value converter or something?
1410 lab = [self makeLabel:label];
1411 [self placeChild:lab on:parent];
1414 CGFloat s = [NSFont systemFontSize] + 4;
1415 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1421 lab = [self makeLabel:low_label];
1422 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1424 [lab setAlignment:1]; // right aligned
1426 if (rect.size.width < LEFT_LABEL_WIDTH)
1427 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1428 rect.size.height = [slider frame].size.height;
1429 [lab setFrame:rect];
1430 [self placeChild:lab on:parent];
1431 # else // USE_IPHONE
1432 [lab setTextAlignment: NSTextAlignmentRight];
1433 // Sometimes rotation screws up truncation.
1434 [lab setLineBreakMode:NSLineBreakByClipping];
1435 [self placeChild:lab on:parent right:(label ? YES : NO)];
1436 # endif // USE_IPHONE
1440 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1441 # else // USE_IPHONE
1442 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1443 # endif // USE_IPHONE
1446 // Make left label be same height as slider.
1448 rect.size.height = [slider frame].size.height;
1449 [lab setFrame:rect];
1453 rect = [slider frame];
1454 if (rect.origin.x < LEFT_LABEL_WIDTH)
1455 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1456 [slider setFrame:rect];
1460 lab = [self makeLabel:high_label];
1461 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1464 // Make right label be same height as slider.
1465 rect.size.height = [slider frame].size.height;
1466 [lab setFrame:rect];
1468 // Sometimes rotation screws up truncation.
1469 [lab setLineBreakMode:NSLineBreakByClipping];
1471 [self placeChild:lab on:parent right:YES];
1474 [self bindSwitch:slider cmdline:arg];
1477 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1479 } else if ([type isEqualToString:@"spinbutton"]) {
1482 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1485 NSAssert1 (!low_label,
1486 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1487 NSAssert1 (!high_label,
1488 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1489 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1493 rect.origin.x = rect.origin.y = 0;
1494 rect.size.width = rect.size.height = 10;
1496 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1497 [txt setStringValue:@"0000.0"];
1499 [txt setStringValue:@""];
1502 LABEL *lab = [self makeLabel:label];
1503 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1504 [lab setAlignment:1]; // right aligned
1506 if (rect.size.width < LEFT_LABEL_WIDTH)
1507 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1508 rect.size.height = [txt frame].size.height;
1509 [lab setFrame:rect];
1510 [self placeChild:lab on:parent];
1513 [self placeChild:txt on:parent right:(label ? YES : NO)];
1517 if (rect.origin.x < LEFT_LABEL_WIDTH)
1518 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1519 [txt setFrame:rect];
1522 rect.size.width = rect.size.height = 10;
1523 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1525 [self placeChild:step on:parent right:YES];
1526 rect = [step frame];
1527 rect.origin.x -= COLUMN_SPACING; // this one goes close
1528 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1529 [step setFrame:rect];
1531 [step setMinValue:[low doubleValue]];
1532 [step setMaxValue:[high doubleValue]];
1533 [step setAutorepeat:YES];
1534 [step setValueWraps:NO];
1536 double range = [high doubleValue] - [low doubleValue];
1538 [step setIncrement:range / 10.0];
1539 else if (range >= 500)
1540 [step setIncrement:range / 100.0];
1542 [step setIncrement:1.0];
1544 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1545 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1546 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1547 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1548 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1549 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1550 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1552 [fmt setGeneratesDecimalNumbers:float_p];
1553 [[txt cell] setFormatter:fmt];
1555 [self bindSwitch:step cmdline:arg];
1556 [self bindSwitch:txt cmdline:arg];
1561 # endif // USE_IPHONE
1564 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1571 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1573 /* If the object associated with this menu item looks like a boolean,
1574 store an NSNumber instead of an NSString, since that's what
1575 will be in the preferences (due to similar logic in PrefsReader).
1577 if ([obj isKindOfClass:[NSString class]]) {
1578 NSString *string = (NSString *) obj;
1579 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1580 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1581 obj = [NSNumber numberWithBool:YES];
1582 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1583 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1584 obj = [NSNumber numberWithBool:NO];
1589 [item setRepresentedObject:obj];
1590 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1592 # endif // !USE_IPHONE
1595 /* Creates the popup menu described by the given XML node (and its children).
1597 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1599 NSArray *children = [node children];
1600 NSUInteger i, count = [children count];
1603 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1607 // get the "id" attribute off the <select> tag.
1609 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1610 [self parseAttrs:dict node:node];
1615 rect.origin.x = rect.origin.y = 0;
1616 rect.size.width = 10;
1617 rect.size.height = 10;
1619 NSString *menu_key = nil; // the resource key used by items in this menu
1622 // #### "Build and Analyze" says that all of our widgets leak, because it
1623 // seems to not realize that placeChild -> addSubview retains them.
1624 // Not sure what to do to make these warnings go away.
1626 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1628 NSMenuItem *def_item = nil;
1629 float max_width = 0;
1631 # else // USE_IPHONE
1633 NSString *def_item = nil;
1635 rect.size.width = 0;
1636 rect.size.height = 0;
1637 # ifdef USE_PICKER_VIEW
1638 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1639 popup.delegate = self;
1640 popup.dataSource = self;
1641 # endif // !USE_PICKER_VIEW
1642 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1644 # endif // USE_IPHONE
1646 for (i = 0; i < count; i++) {
1647 NSXMLNode *child = [children objectAtIndex:i];
1649 if ([child kind] == NSXMLCommentKind)
1651 if ([child kind] != NSXMLElementKind) {
1652 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1656 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1658 NSMutableDictionary *dict2 = [@{ @"id": @"",
1662 [self parseAttrs:dict2 node:child];
1663 NSString *label = [dict2 objectForKey:@"_label"];
1664 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1669 NSAssert1 (0, @"no _label in %@", [child name]);
1674 // create the menu item (and then get a pointer to it)
1675 [popup addItemWithTitle:label];
1676 NSMenuItem *item = [popup itemWithTitle:label];
1677 # endif // USE_IPHONE
1680 NSString *this_val = NULL;
1681 NSString *this_key = [self switchToResource: arg_set
1684 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1685 if (menu_key && ![menu_key isEqualToString:this_key])
1687 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1688 menu_key, this_key, this_val);
1690 menu_key = this_key;
1692 /* If this menu has the cmd line "-mode foo" then set this item's
1693 value to "foo" (the menu itself will be bound to e.g. "modeString")
1696 set_menu_item_object (item, this_val);
1698 // Array holds ["Label", "resource-key", "resource-val"].
1699 [items addObject:[NSMutableArray arrayWithObjects:
1700 label, @"", this_val, nil]];
1704 // no arg-set -- only one menu item can be missing that.
1705 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1710 // Array holds ["Label", "resource-key", "resource-val"].
1711 [items addObject:[NSMutableArray arrayWithObjects:
1712 label, @"", @"", nil]];
1716 /* make sure the menu button has room for the text of this item,
1717 and remember the greatest width it has reached.
1720 [popup setTitle:label];
1722 NSRect r = [popup frame];
1723 if (r.size.width > max_width) max_width = r.size.width;
1724 # endif // USE_IPHONE
1728 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1732 /* We've added all of the menu items. If there was an item with no
1733 command-line switch, then it's the item that represents the default
1734 value. Now we must bind to that item as well... (We have to bind
1735 this one late, because if it was the first item, then we didn't
1736 yet know what resource was associated with this menu.)
1739 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1741 @"no default value for resource \"%@\" in menu item \"%@\"",
1751 set_menu_item_object (def_item, def_obj);
1752 # else // !USE_IPHONE
1753 for (NSMutableArray *a in items) {
1754 // Make sure each array contains the resource key.
1755 [a replaceObjectAtIndex:1 withObject:menu_key];
1756 // Make sure the default item contains the default resource value.
1757 if (def_obj && def_item &&
1758 [def_item isEqualToString:[a objectAtIndex:0]])
1759 [a replaceObjectAtIndex:2 withObject:def_obj];
1761 # endif // !USE_IPHONE
1765 # ifdef USE_PICKER_VIEW
1766 /* Finish tweaking the menu button itself.
1769 [popup setTitle:[def_item title]];
1770 NSRect r = [popup frame];
1771 r.size.width = max_width;
1773 # endif // USE_PICKER_VIEW
1776 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1777 [self placeChild:popup on:parent];
1778 [self bindResource:popup key:menu_key];
1783 # ifdef USE_PICKER_VIEW
1784 // Store the items for this picker in the picker_values array.
1785 // This is so fucking stupid.
1787 unsigned long menu_number = [pref_keys count] - 1;
1788 if (! picker_values)
1789 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1790 while ([picker_values count] <= menu_number)
1791 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1792 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1793 [popup reloadAllComponents];
1795 # else // !USE_PICKER_VIEW
1797 [self placeSeparator];
1800 for (__attribute__((unused)) NSArray *item in items) {
1801 RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1803 [b setLineBreakMode:NSLineBreakByTruncatingHead];
1804 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1805 [self placeChild:b on:parent];
1810 [self placeSeparator];
1812 # endif // !USE_PICKER_VIEW
1813 # endif // !USE_IPHONE
1818 /* Creates an uneditable, wrapping NSTextField to display the given
1819 text enclosed by <description> ... </description> in the XML.
1821 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1823 NSString *text = nil;
1824 NSArray *children = [node children];
1825 NSUInteger i, count = [children count];
1827 for (i = 0; i < count; i++) {
1828 NSXMLNode *child = [children objectAtIndex:i];
1829 NSString *s = [child objectValue];
1831 text = [text stringByAppendingString:s];
1836 text = unwrap (text);
1838 NSRect rect = [parent frame];
1839 rect.origin.x = rect.origin.y = 0;
1840 rect.size.width = 200;
1841 rect.size.height = 50; // sized later
1843 NSText *lab = [[NSText alloc] initWithFrame:rect];
1845 [lab setEditable:NO];
1846 [lab setDrawsBackground:NO];
1847 [lab setHorizontallyResizable:YES];
1848 [lab setVerticallyResizable:YES];
1849 [lab setString:text];
1854 # else // USE_IPHONE
1856 # ifndef USE_HTML_LABELS
1858 UILabel *lab = [self makeLabel:text];
1859 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1862 # else // USE_HTML_LABELS
1863 HTMLLabel *lab = [[HTMLLabel alloc]
1865 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1867 [lab setFrame:rect];
1869 # endif // USE_HTML_LABELS
1871 [self placeSeparator];
1873 # endif // USE_IPHONE
1875 [self placeChild:lab on:parent];
1879 /* Creates the NSTextField described by the given XML node.
1881 - (void) makeTextField: (NSXMLNode *)node
1882 on: (NSView *)parent
1883 withLabel: (BOOL) label_p
1884 horizontal: (BOOL) horiz_p
1886 NSMutableDictionary *dict = [@{ @"id": @"",
1890 [self parseAttrs:dict node:node];
1891 NSString *label = [dict objectForKey:@"_label"];
1892 NSString *arg = [dict objectForKey:@"arg"];
1896 if (!label && label_p) {
1897 NSAssert1 (0, @"no _label in %@", [node name]);
1901 NSAssert1 (arg, @"no arg in %@", label);
1904 rect.origin.x = rect.origin.y = 0;
1905 rect.size.width = rect.size.height = 10;
1907 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1911 // make the default size be around 30 columns; a typical value for
1912 // these text fields is "xscreensaver-text --cols 40".
1914 [txt setStringValue:@"123456789 123456789 123456789 "];
1916 [[txt cell] setWraps:NO];
1917 [[txt cell] setScrollable:YES];
1918 [txt setStringValue:@""];
1920 # else // USE_IPHONE
1922 txt.adjustsFontSizeToFitWidth = YES;
1923 txt.textColor = [UIColor blackColor];
1924 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1925 txt.placeholder = @"";
1926 txt.borderStyle = UITextBorderStyleRoundedRect;
1927 txt.textAlignment = NSTextAlignmentRight;
1928 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1929 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1930 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1931 txt.clearButtonMode = UITextFieldViewModeAlways;
1932 txt.returnKeyType = UIReturnKeyDone;
1933 txt.delegate = self;
1935 [txt setEnabled: YES];
1937 rect.size.height = [txt.font lineHeight] * 1.2;
1938 [txt setFrame:rect];
1940 # endif // USE_IPHONE
1943 LABEL *lab = [self makeLabel:label];
1944 [self placeChild:lab on:parent];
1947 [self placeChild:txt on:parent right:(label ? YES : NO)];
1949 [self bindSwitch:txt cmdline:arg];
1954 /* Creates the NSTextField described by the given XML node,
1955 and hooks it up to a Choose button and a file selector widget.
1957 - (void) makeFileSelector: (NSXMLNode *)node
1958 on: (NSView *)parent
1959 dirsOnly: (BOOL) dirsOnly
1960 withLabel: (BOOL) label_p
1961 editable: (BOOL) editable_p
1963 # ifndef USE_IPHONE // No files. No selectors.
1964 NSMutableDictionary *dict = [@{ @"id": @"",
1968 [self parseAttrs:dict node:node];
1969 NSString *label = [dict objectForKey:@"_label"];
1970 NSString *arg = [dict objectForKey:@"arg"];
1974 if (!label && label_p) {
1975 NSAssert1 (0, @"no _label in %@", [node name]);
1979 NSAssert1 (arg, @"no arg in %@", label);
1982 rect.origin.x = rect.origin.y = 0;
1983 rect.size.width = rect.size.height = 10;
1985 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1987 // make the default size be around 20 columns.
1989 [txt setStringValue:@"123456789 123456789 "];
1991 [txt setSelectable:YES];
1992 [txt setEditable:editable_p];
1993 [txt setBezeled:editable_p];
1994 [txt setDrawsBackground:editable_p];
1995 [[txt cell] setWraps:NO];
1996 [[txt cell] setScrollable:YES];
1997 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1998 [txt setStringValue:@""];
2002 lab = [self makeLabel:label];
2003 [self placeChild:lab on:parent];
2006 [self placeChild:txt on:parent right:(label ? YES : NO)];
2008 [self bindSwitch:txt cmdline:arg];
2011 // Make the text field and label be the same height, whichever is taller.
2014 rect.size.height = ([lab frame].size.height > [txt frame].size.height
2015 ? [lab frame].size.height
2016 : [txt frame].size.height);
2017 [txt setFrame:rect];
2020 // Now put a "Choose" button next to it.
2022 rect.origin.x = rect.origin.y = 0;
2023 rect.size.width = rect.size.height = 10;
2024 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
2025 [choose setTitle:@"Choose..."];
2026 [choose setBezelStyle:NSRoundedBezelStyle];
2029 [self placeChild:choose on:parent right:YES];
2031 // center the Choose button around the midpoint of the text field.
2032 rect = [choose frame];
2033 rect.origin.y = ([txt frame].origin.y +
2034 (([txt frame].size.height - rect.size.height) / 2));
2035 [choose setFrameOrigin:rect.origin];
2037 [choose setTarget:[parent window]];
2039 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
2041 [choose setAction:@selector(fileSelectorChooseAction:)];
2044 # endif // !USE_IPHONE
2050 /* Runs a modal file selector and sets the text field's value to the
2051 selected file or directory.
2054 do_file_selector (NSTextField *txt, BOOL dirs_p)
2056 NSOpenPanel *panel = [NSOpenPanel openPanel];
2057 [panel setAllowsMultipleSelection:NO];
2058 [panel setCanChooseFiles:!dirs_p];
2059 [panel setCanChooseDirectories:dirs_p];
2061 int result = [panel runModal];
2062 if (result == NSOKButton) {
2063 NSArray *files = [panel URLs];
2064 NSString *file = ([files count] > 0 ? [[files objectAtIndex:0] path] : @"");
2065 file = [file stringByAbbreviatingWithTildeInPath];
2066 [txt setStringValue:file];
2068 // Fuck me! Just setting the value of the NSTextField does not cause
2069 // that to end up in the preferences!
2071 NSDictionary *dict = [txt infoForBinding:@"value"];
2072 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
2073 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
2074 if ([path hasPrefix:@"values."]) // WTF.
2075 path = [path substringFromIndex:7];
2076 [[prefs values] setValue:file forKey:path];
2081 /* Returns the NSTextField that is to the left of or above the NSButton.
2083 static NSTextField *
2084 find_text_field_of_button (NSButton *button)
2086 NSView *parent = [button superview];
2087 NSArray *kids = [parent subviews];
2088 int nkids = [kids count];
2091 for (i = 0; i < nkids; i++) {
2092 NSObject *kid = [kids objectAtIndex:i];
2093 if ([kid isKindOfClass:[NSTextField class]]) {
2094 f = (NSTextField *) kid;
2095 } else if (kid == button) {
2104 - (void) fileSelectorChooseAction:(NSObject *)arg
2106 NSButton *choose = (NSButton *) arg;
2107 NSTextField *txt = find_text_field_of_button (choose);
2108 do_file_selector (txt, NO);
2111 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2113 NSButton *choose = (NSButton *) arg;
2114 NSTextField *txt = find_text_field_of_button (choose);
2115 do_file_selector (txt, YES);
2118 #endif // !USE_IPHONE
2121 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2126 (x) Computer name and time
2127 ( ) Text [__________________________]
2128 ( ) Text file [_________________] [Choose]
2129 ( ) URL [__________________________]
2130 ( ) Shell Cmd [__________________________]
2132 textMode -text-mode date
2133 textMode -text-mode literal textLiteral -text-literal %
2134 textMode -text-mode file textFile -text-file %
2135 textMode -text-mode url textURL -text-url %
2136 textMode -text-mode program textProgram -text-program %
2139 rect.size.width = rect.size.height = 1;
2140 rect.origin.x = rect.origin.y = 0;
2141 NSView *group = [[NSView alloc] initWithFrame:rect];
2142 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2144 Bool program_p = TRUE;
2149 // This is how you link radio buttons together.
2151 NSButtonCell *proto = [[NSButtonCell alloc] init];
2152 [proto setButtonType:NSRadioButton];
2154 rect.origin.x = rect.origin.y = 0;
2155 rect.size.width = rect.size.height = 10;
2156 NSMatrix *matrix = [[NSMatrix alloc]
2158 mode:NSRadioModeMatrix
2160 numberOfRows: 4 + (program_p ? 1 : 0)
2162 [matrix setAllowsEmptySelection:NO];
2164 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
2165 [cnames addObject:@"Computer name and time"];
2166 [cnames addObject:@"Text"];
2167 [cnames addObject:@"File"];
2168 [cnames addObject:@"URL"];
2169 if (program_p) [cnames addObject:@"Shell Cmd"];
2170 [matrix bind:@"content"
2172 withKeyPath:@"arrangedObjects"
2176 [self bindSwitch:matrix cmdline:@"-text-mode %"];
2178 [self placeChild:matrix on:group];
2179 [self placeChild:rgroup on:group right:YES];
2186 # else // USE_IPHONE
2188 NSView *rgroup = parent;
2191 // <select id="textMode">
2192 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
2193 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
2194 // <option id="url" _label="Display URL"/>
2197 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2198 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2200 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2201 [node3 setAttributesAsDictionary:
2203 @"arg-set": @"-text-mode date",
2204 @"_label": @"Display the date and time" }];
2205 [node3 setParent: node2];
2206 [node3 autorelease];
2208 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2209 [node3 setAttributesAsDictionary:
2211 @"arg-set": @"-text-mode literal",
2212 @"_label": @"Display static text" }];
2213 [node3 setParent: node2];
2214 [node3 autorelease];
2216 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2217 [node3 setAttributesAsDictionary:
2219 @"_label": @"Display the contents of a URL" }];
2220 [node3 setParent: node2];
2221 [node3 autorelease];
2223 [self makeOptionMenu:node2 on:rgroup];
2226 # endif // USE_IPHONE
2229 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2230 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2231 [node2 setAttributesAsDictionary:
2232 @{ @"id": @"textLiteral",
2233 @"arg": @"-text-literal %",
2235 @"_label": @"Text to display"
2238 [self makeTextField:node2 on:rgroup
2247 // rect = [last_child(rgroup) frame];
2249 /* // trying to make the text fields be enabled only when the checkbox is on..
2250 control = last_child (rgroup);
2251 [control bind:@"enabled"
2252 toObject:[matrix cellAtRow:1 column:0]
2253 withKeyPath:@"value"
2259 // <file id="textFile" _label="" arg-set="-text-file %"/>
2260 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2261 [node2 setAttributesAsDictionary:
2262 @{ @"id": @"textFile",
2263 @"arg": @"-text-file %" }];
2264 [self makeFileSelector:node2 on:rgroup
2265 dirsOnly:NO withLabel:NO editable:NO];
2267 # endif // !USE_IPHONE
2269 // rect = [last_child(rgroup) frame];
2271 // <string id="textURL" _label="" arg-set="text-url %"/>
2272 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2273 [node2 setAttributesAsDictionary:
2274 @{ @"id": @"textURL",
2275 @"arg": @"-text-url %",
2277 @"_label": @"URL to display",
2280 [self makeTextField:node2 on:rgroup
2289 // rect = [last_child(rgroup) frame];
2293 // <string id="textProgram" _label="" arg-set="text-program %"/>
2294 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2295 [node2 setAttributesAsDictionary:
2296 @{ @"id": @"textProgram",
2297 @"arg": @"-text-program %",
2299 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2303 // rect = [last_child(rgroup) frame];
2305 layout_group (rgroup, NO);
2307 rect = [rgroup frame];
2308 rect.size.width += 35; // WTF? Why is rgroup too narrow?
2309 [rgroup setFrame:rect];
2312 // Set the height of the cells in the radio-box matrix to the height of
2313 // the (last of the) text fields.
2314 control = last_child (rgroup);
2315 rect = [control frame];
2316 rect.size.width = 30; // width of the string "Text", plus a bit...
2318 rect.size.width += 25;
2319 rect.size.height += LINE_SPACING;
2320 [matrix setCellSize:rect.size];
2321 [matrix sizeToCells];
2323 layout_group (group, YES);
2324 rect = [matrix frame];
2325 rect.origin.x += rect.size.width + COLUMN_SPACING;
2326 rect.origin.y -= [control frame].size.height - LINE_SPACING;
2327 [rgroup setFrameOrigin:rect.origin];
2329 // now cheat on the size of the matrix: allow it to overlap (underlap)
2332 rect.size = [matrix cellSize];
2333 rect.size.width = 300;
2334 [matrix setCellSize:rect.size];
2335 [matrix sizeToCells];
2337 // Cheat on the position of the stuff on the right (the rgroup).
2338 // GAAAH, this code is such crap!
2339 rect = [rgroup frame];
2341 [rgroup setFrame:rect];
2344 rect.size.width = rect.size.height = 0;
2345 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2346 [box setTitlePosition:NSAtTop];
2347 [box setBorderType:NSBezelBorder];
2348 [box setTitle:@"Display Text"];
2350 rect.size.width = rect.size.height = 12;
2351 [box setContentViewMargins:rect.size];
2352 [box setContentView:group];
2355 [self placeChild:box on:parent];
2359 # endif // !USE_IPHONE
2363 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2366 [x] Grab desktop images
2367 [ ] Choose random image:
2368 [__________________________] [Choose]
2370 <boolean id="grabDesktopImages" _label="Grab desktop images"
2371 arg-unset="-no-grab-desktop"/>
2372 <boolean id="chooseRandomImages" _label="Grab desktop images"
2373 arg-unset="-choose-random-images"/>
2374 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2377 NSXMLElement *node2;
2380 # define SCREENS "Grab desktop images"
2381 # define PHOTOS "Choose random images"
2383 # define SCREENS "Grab screenshots"
2384 # define PHOTOS "Use photo library"
2387 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2388 [node2 setAttributesAsDictionary:
2389 @{ @"id": @"grabDesktopImages",
2390 @"_label": @ SCREENS,
2391 @"arg-unset": @"-no-grab-desktop",
2393 [self makeCheckbox:node2 on:parent];
2396 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2397 [node2 setAttributesAsDictionary:
2398 @{ @"id": @"chooseRandomImages",
2399 @"_label": @ PHOTOS,
2400 @"arg-set": @"-choose-random-images",
2402 [self makeCheckbox:node2 on:parent];
2405 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2406 [node2 setAttributesAsDictionary:
2407 @{ @"id": @"imageDirectory",
2408 @"_label": @"Images from:",
2409 @"arg": @"-image-directory %",
2411 [self makeFileSelector:node2 on:parent
2412 dirsOnly:YES withLabel:YES editable:YES];
2419 // Add a second, explanatory label below the file/URL selector.
2422 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2423 [self placeChild:lab2 on:parent];
2425 // Pack it in a little tighter vertically.
2426 NSRect r2 = [lab2 frame];
2429 [lab2 setFrameOrigin:r2.origin];
2430 # endif // USE_IPHONE
2434 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2438 [x] Check for Updates [ Monthly ]
2441 <boolean id="automaticallyChecksForUpdates"
2442 _label="Automatically check for updates"
2443 arg-unset="-no-automaticallyChecksForUpdates" />
2444 <select id="updateCheckInterval">
2445 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2446 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/>
2447 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2448 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2456 rect.size.width = rect.size.height = 1;
2457 rect.origin.x = rect.origin.y = 0;
2458 NSView *group = [[NSView alloc] initWithFrame:rect];
2460 NSXMLElement *node2;
2464 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2465 [node2 setAttributesAsDictionary:
2466 @{ @"id": @SUSUEnableAutomaticChecksKey,
2467 @"_label": @"Automatically check for updates",
2468 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2470 [self makeCheckbox:node2 on:group];
2475 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2476 [node2 setAttributesAsDictionary:
2477 @{ @"id": @SUScheduledCheckIntervalKey }];
2481 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2482 [node3 setAttributesAsDictionary:
2483 @{ @"id": @"hourly",
2484 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2485 @"_label": @"Hourly" }];
2486 [node3 setParent: node2];
2487 [node3 autorelease];
2489 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2490 [node3 setAttributesAsDictionary:
2492 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2493 @"_label": @"Daily" }];
2494 [node3 setParent: node2];
2495 [node3 autorelease];
2497 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2498 [node3 setAttributesAsDictionary:
2499 @{ @"id": @"weekly",
2500 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2501 @"_label": @"Weekly",
2503 [node3 setParent: node2];
2504 [node3 autorelease];
2506 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2507 [node3 setAttributesAsDictionary:
2508 @{ @"id": @"monthly",
2509 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2510 @"_label": @"Monthly",
2512 [node3 setParent: node2];
2513 [node3 autorelease];
2516 [self makeOptionMenu:node2 on:group];
2520 layout_group (group, TRUE);
2522 rect.size.width = rect.size.height = 0;
2523 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2524 [box setTitlePosition:NSNoTitle];
2525 [box setBorderType:NSNoBorder];
2526 [box setContentViewMargins:rect.size];
2527 [box setContentView:group];
2530 [self placeChild:box on:parent];
2535 # endif // !USE_IPHONE
2539 #pragma mark Layout for controls
2544 last_child (NSView *parent)
2546 NSArray *kids = [parent subviews];
2547 int nkids = [kids count];
2551 return [kids objectAtIndex:nkids-1];
2553 #endif // USE_IPHONE
2556 /* Add the child as a subview of the parent, positioning it immediately
2557 below or to the right of the previously-added child of that view.
2559 - (void) placeChild:
2565 on:(NSView *)parent right:(BOOL)right_p
2568 NSRect rect = [child frame];
2569 NSView *last = last_child (parent);
2571 rect.origin.x = LEFT_MARGIN;
2572 rect.origin.y = ([parent frame].size.height - rect.size.height
2574 } else if (right_p) {
2575 rect = [last frame];
2576 rect.origin.x += rect.size.width + COLUMN_SPACING;
2578 rect = [last frame];
2579 rect.origin.x = LEFT_MARGIN;
2580 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2582 NSRect r = [child frame];
2583 r.origin = rect.origin;
2585 [parent addSubview:child];
2587 # else // USE_IPHONE
2589 /* Controls is an array of arrays of the controls, divided into sections.
2590 Each hgroup / vgroup gets a nested array, too, e.g.:
2592 [ [ [ <label>, <checkbox> ],
2593 [ <label>, <checkbox> ],
2594 [ <label>, <checkbox> ] ],
2595 [ <label>, <text-field> ],
2596 [ <label>, <low-label>, <slider>, <high-label> ],
2597 [ <low-label>, <slider>, <high-label> ],
2601 If an element begins with a label, it is terminal, otherwise it is a
2602 group. There are (currently) never more than 4 elements in a single
2605 A blank vertical spacer is placed between each hgroup / vgroup,
2606 by making each of those a new section in the TableView.
2609 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2610 if ([controls count] == 0)
2611 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2612 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2614 if (!right_p || [current count] == 0) {
2615 // Nothing on the current line. Add this object.
2616 [current addObject: child];
2618 // Something's on the current line already.
2619 NSObject *old = [current objectAtIndex:[current count]-1];
2620 if ([old isKindOfClass:[NSMutableArray class]]) {
2621 // Already an array in this cell. Append.
2622 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2623 [(NSMutableArray *) old addObject: child];
2625 // Replace the control in this cell with an array, then append
2626 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2627 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2630 # endif // USE_IPHONE
2634 - (void) placeChild:(NSView *)child on:(NSView *)parent
2636 [self placeChild:child on:parent right:NO];
2642 // Start putting subsequent children in a new group, to create a new
2643 // section on the UITableView.
2645 - (void) placeSeparator
2647 if (! controls) return;
2648 if ([controls count] == 0) return;
2649 if ([[controls objectAtIndex:[controls count]-1]
2651 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2653 #endif // USE_IPHONE
2657 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2658 wrapped in <hgroup> or <vgroup> in the XML.
2660 - (void) makeGroup:(NSXMLNode *)node
2662 horizontal:(BOOL) horiz_p
2665 if (!horiz_p) [self placeSeparator];
2666 [self traverseChildren:node on:parent];
2667 if (!horiz_p) [self placeSeparator];
2668 # else // !USE_IPHONE
2670 rect.size.width = rect.size.height = 1;
2671 rect.origin.x = rect.origin.y = 0;
2672 NSView *group = [[NSView alloc] initWithFrame:rect];
2673 [self traverseChildren:node on:group];
2675 layout_group (group, horiz_p);
2677 rect.size.width = rect.size.height = 0;
2678 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2679 [box setTitlePosition:NSNoTitle];
2680 [box setBorderType:NSNoBorder];
2681 [box setContentViewMargins:rect.size];
2682 [box setContentView:group];
2685 [self placeChild:box on:parent];
2688 # endif // !USE_IPHONE
2694 layout_group (NSView *group, BOOL horiz_p)
2696 NSArray *kids = [group subviews];
2697 int nkids = [kids count];
2699 double maxx = 0, miny = 0;
2700 for (i = 0; i < nkids; i++) {
2701 NSView *kid = [kids objectAtIndex:i];
2702 NSRect r = [kid frame];
2705 maxx += r.size.width + COLUMN_SPACING;
2706 if (r.size.height > -miny) miny = -r.size.height;
2708 if (r.size.width > maxx) maxx = r.size.width;
2709 miny = r.origin.y - r.size.height;
2716 rect.size.width = maxx;
2717 rect.size.height = -miny;
2718 [group setFrame:rect];
2721 for (i = 0; i < nkids; i++) {
2722 NSView *kid = [kids objectAtIndex:i];
2723 NSRect r = [kid frame];
2725 r.origin.y = rect.size.height - r.size.height;
2727 x += r.size.width + COLUMN_SPACING;
2734 #endif // !USE_IPHONE
2737 /* Create some kind of control corresponding to the given XML node.
2739 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2741 NSString *name = [node name];
2743 if ([node kind] == NSXMLCommentKind)
2746 if ([node kind] == NSXMLTextKind) {
2747 NSString *s = [(NSString *) [node objectValue]
2748 stringByTrimmingCharactersInSet:
2749 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2750 if (! [s isEqualToString:@""]) {
2751 NSAssert1 (0, @"unexpected text: %@", s);
2756 if ([node kind] != NSXMLElementKind) {
2757 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2761 if ([name isEqualToString:@"hgroup"] ||
2762 [name isEqualToString:@"vgroup"]) {
2764 [self makeGroup:node on:parent
2765 horizontal:[name isEqualToString:@"hgroup"]];
2767 } else if ([name isEqualToString:@"command"]) {
2768 // do nothing: this is the "-root" business
2770 } else if ([name isEqualToString:@"video"]) {
2773 } else if ([name isEqualToString:@"boolean"]) {
2774 [self makeCheckbox:node on:parent];
2776 } else if ([name isEqualToString:@"string"]) {
2777 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2779 } else if ([name isEqualToString:@"file"]) {
2780 [self makeFileSelector:node on:parent
2781 dirsOnly:NO withLabel:YES editable:NO];
2783 } else if ([name isEqualToString:@"number"]) {
2784 [self makeNumberSelector:node on:parent];
2786 } else if ([name isEqualToString:@"select"]) {
2787 [self makeOptionMenu:node on:parent];
2789 } else if ([name isEqualToString:@"_description"]) {
2790 [self makeDescLabel:node on:parent];
2792 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2793 [self makeTextLoaderControlBox:node on:parent];
2795 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2796 [self makeImageLoaderControlBox:node on:parent];
2798 } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2799 [self makeUpdaterControlBox:node on:parent];
2802 NSAssert1 (0, @"unknown tag: %@", name);
2807 /* Iterate over and process the children of this XML node.
2809 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2811 NSArray *children = [node children];
2812 NSUInteger i, count = [children count];
2813 for (i = 0; i < count; i++) {
2814 NSXMLNode *child = [children objectAtIndex:i];
2815 [self makeControl:child on:parent];
2822 /* Kludgey magic to make the window enclose the controls we created.
2825 fix_contentview_size (NSView *parent)
2828 NSArray *kids = [parent subviews];
2829 int nkids = [kids count];
2830 NSView *text = 0; // the NSText at the bottom of the window
2831 double maxx = 0, miny = 0;
2834 /* Find the size of the rectangle taken up by each of the children
2835 except the final "NSText" child.
2837 for (i = 0; i < nkids; i++) {
2838 NSView *kid = [kids objectAtIndex:i];
2839 if ([kid isKindOfClass:[NSText class]]) {
2844 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2845 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2846 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2847 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2848 // f.origin.y + f.size.height, [kid class]);
2851 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2853 /* Now that we know the width of the window, set the width of the NSText to
2854 that, so that it can decide what its height needs to be.
2856 if (! text) abort();
2858 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2859 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2860 // f.origin.y + f.size.height, [text class]);
2862 // set the NSText's width (this changes its height).
2863 f.size.width = maxx - LEFT_MARGIN;
2866 // position the NSText below the last child (this gives us a new miny).
2868 f.origin.y = miny - f.size.height - LINE_SPACING;
2869 miny = f.origin.y - LINE_SPACING;
2872 // Lock the width of the field and unlock the height, and let it resize
2873 // once more, to compute the proper height of the text for that width.
2875 [(NSText *) text setHorizontallyResizable:NO];
2876 [(NSText *) text setVerticallyResizable:YES];
2877 [(NSText *) text sizeToFit];
2879 // Now lock the height too: no more resizing this text field.
2881 [(NSText *) text setVerticallyResizable:NO];
2883 // Now reposition the top edge of the text field to be back where it
2884 // was before we changed the height.
2886 float oh = f.size.height;
2888 float dh = f.size.height - oh;
2891 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2892 // If we do this in 10.6, the text field moves down, off the window.
2893 // So instead we repair it at the end, at the "WTF2" comment.
2896 // Also adjust the parent height by the change in height of the text field.
2899 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2900 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2901 // f.origin.y + f.size.height, [text class]);
2904 /* Set the contentView to the size of the children.
2907 // float yoff = f.size.height;
2908 f.size.width = maxx + LEFT_MARGIN;
2909 f.size.height = -(miny - LEFT_MARGIN*2);
2910 // yoff = f.size.height - yoff;
2911 [parent setFrame:f];
2913 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2914 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2916 /* Now move all of the kids up into the window.
2919 float shift = f.size.height;
2920 // NSLog(@"shift: %3.0f", shift);
2921 for (i = 0; i < nkids; i++) {
2922 NSView *kid = [kids objectAtIndex:i];
2924 f.origin.y += shift;
2926 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2927 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2928 // f.origin.y + f.size.height, [kid class]);
2933 parent: 420 x 541 @ 0 0
2934 text: 380 x 100 @ 20 22 miny=-501
2937 parent: 420 x 541 @ 0 0
2938 text: 380 x 100 @ 20 50 miny=-501
2941 // #### WTF2: See "WTF" above. If the text field is off the screen,
2942 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2945 if (f.origin.y < 50) { // magic numbers, yay
2950 /* Set the kids to track the top left corner of the window when resized.
2951 Set the NSText to track the bottom right corner as well.
2953 for (i = 0; i < nkids; i++) {
2954 NSView *kid = [kids objectAtIndex:i];
2955 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2956 if ([kid isKindOfClass:[NSText class]])
2957 mask |= NSViewWidthSizable|NSViewHeightSizable;
2958 [kid setAutoresizingMask:mask];
2961 # endif // !USE_IPHONE
2967 wrap_with_buttons (NSWindow *window, NSView *panel)
2971 // Make a box to hold the buttons at the bottom of the window.
2973 rect = [panel frame];
2974 rect.origin.x = rect.origin.y = 0;
2975 rect.size.height = 10;
2976 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2977 [bbox setTitlePosition:NSNoTitle];
2978 [bbox setBorderType:NSNoBorder];
2980 // Make some buttons: Default, Cancel, OK
2982 rect.origin.x = rect.origin.y = 0;
2983 rect.size.width = rect.size.height = 10;
2984 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2985 [reset setTitle:@"Reset to Defaults"];
2986 [reset setBezelStyle:NSRoundedBezelStyle];
2989 rect = [reset frame];
2990 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2991 [ok setTitle:@"OK"];
2992 [ok setBezelStyle:NSRoundedBezelStyle];
2994 rect = [bbox frame];
2995 rect.origin.x = rect.size.width - [ok frame].size.width;
2996 [ok setFrameOrigin:rect.origin];
2999 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
3000 [cancel setTitle:@"Cancel"];
3001 [cancel setBezelStyle:NSRoundedBezelStyle];
3003 rect.origin.x -= [cancel frame].size.width + 10;
3004 [cancel setFrameOrigin:rect.origin];
3006 // Bind OK to RET and Cancel to ESC.
3007 [ok setKeyEquivalent:@"\r"];
3008 [cancel setKeyEquivalent:@"\e"];
3010 // The correct width for OK and Cancel buttons is 68 pixels
3011 // ("Human Interface Guidelines: Controls: Buttons:
3012 // Push Button Specifications").
3015 rect.size.width = 68;
3018 rect = [cancel frame];
3019 rect.size.width = 68;
3020 [cancel setFrame:rect];
3022 // It puts the buttons in the box or else it gets the hose again
3024 [bbox addSubview:ok];
3025 [bbox addSubview:cancel];
3026 [bbox addSubview:reset];
3029 // make a box to hold the button-box, and the preferences view
3031 rect = [bbox frame];
3032 rect.origin.y += rect.size.height;
3033 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
3034 [pbox setTitlePosition:NSNoTitle];
3035 [pbox setBorderType:NSBezelBorder];
3037 // Enforce a max height on the dialog, so that it's obvious to me
3038 // (on a big screen) when the dialog will fall off the bottom of
3039 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
3041 NSRect f = [panel frame];
3042 int screen_height = (768 // shortest "modern" Mac display
3044 - 56 // System Preferences toolbar
3045 - 140 // default magnified bottom dock icon
3047 if (f.size.height > screen_height) {
3048 NSLog(@"%@ height was %.0f; clipping to %d",
3049 [panel class], f.size.height, screen_height);
3050 f.size.height = screen_height;
3055 [pbox addSubview:panel];
3056 [pbox addSubview:bbox];
3059 [reset setAutoresizingMask:NSViewMaxXMargin];
3060 [cancel setAutoresizingMask:NSViewMinXMargin];
3061 [ok setAutoresizingMask:NSViewMinXMargin];
3062 [bbox setAutoresizingMask:NSViewWidthSizable];
3066 [ok setTarget:window];
3067 [cancel setTarget:window];
3068 [reset setTarget:window];
3069 [ok setAction:@selector(okAction:)];
3070 [cancel setAction:@selector(cancelAction:)];
3071 [reset setAction:@selector(resetAction:)];
3077 #endif // !USE_IPHONE
3080 /* Iterate over and process the children of the root node of the XML document.
3082 - (void)traverseTree
3085 NSView *parent = [self view];
3087 NSWindow *parent = self;
3089 NSXMLNode *node = xml_root;
3091 if (![[node name] isEqualToString:@"screensaver"]) {
3092 NSAssert (0, @"top level node is not <xscreensaver>");
3095 saver_name = [self parseXScreenSaverTag: node];
3096 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
3098 [saver_name retain];
3103 rect.origin.x = rect.origin.y = 0;
3104 rect.size.width = rect.size.height = 1;
3106 NSView *panel = [[NSView alloc] initWithFrame:rect];
3107 [self traverseChildren:node on:panel];
3108 fix_contentview_size (panel);
3110 NSView *root = wrap_with_buttons (parent, panel);
3111 [userDefaultsController setAppliesImmediately:NO];
3112 [globalDefaultsController setAppliesImmediately:NO];
3114 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
3116 rect = [parent frameRectForContentRect:[root frame]];
3117 [parent setFrame:rect display:NO];
3118 [parent setMinSize:rect.size];
3120 [parent setContentView:root];
3125 # else // USE_IPHONE
3127 CGRect r = [parent frame];
3128 r.size = [[UIScreen mainScreen] bounds].size;
3129 [parent setFrame:r];
3130 [self traverseChildren:node on:parent];
3132 # endif // USE_IPHONE
3136 - (void)parser:(NSXMLParser *)parser
3137 didStartElement:(NSString *)elt
3138 namespaceURI:(NSString *)ns
3139 qualifiedName:(NSString *)qn
3140 attributes:(NSDictionary *)attrs
3142 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3144 [e setKind:SimpleXMLElementKind];
3145 [e setAttributesAsDictionary:attrs];
3146 NSXMLElement *p = xml_parsing;
3150 xml_root = xml_parsing;
3153 - (void)parser:(NSXMLParser *)parser
3154 didEndElement:(NSString *)elt
3155 namespaceURI:(NSString *)ns
3156 qualifiedName:(NSString *)qn
3158 NSXMLElement *p = xml_parsing;
3160 NSLog(@"extra close: %@", elt);
3161 } else if (![[p name] isEqualToString:elt]) {
3162 NSLog(@"%@ closed by %@", [p name], elt);
3164 NSXMLElement *n = xml_parsing;
3165 xml_parsing = [n parent];
3170 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3172 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3173 [e setKind:SimpleXMLTextKind];
3174 NSXMLElement *p = xml_parsing;
3176 [e setObjectValue: string];
3182 # ifdef USE_PICKER_VIEW
3184 #pragma mark UIPickerView delegate methods
3186 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3188 return 1; // Columns
3191 - (NSInteger)pickerView:(UIPickerView *)pv
3192 numberOfRowsInComponent:(NSInteger)column
3194 NSAssert (column == 0, @"weird column");
3195 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3196 if (! a) return 0; // Too early?
3200 - (CGFloat)pickerView:(UIPickerView *)pv
3201 rowHeightForComponent:(NSInteger)column
3206 - (CGFloat)pickerView:(UIPickerView *)pv
3207 widthForComponent:(NSInteger)column
3209 NSAssert (column == 0, @"weird column");
3210 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3211 if (! a) return 0; // Too early?
3213 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3215 for (NSArray *a2 in a) {
3216 NSString *s = [a2 objectAtIndex:0];
3217 CGSize r = [s sizeWithFont:f];
3218 if (r.width > max) max = r.width;
3221 max *= 1.7; // WTF!!
3233 - (NSString *)pickerView:(UIPickerView *)pv
3234 titleForRow:(NSInteger)row
3235 forComponent:(NSInteger)column
3237 NSAssert (column == 0, @"weird column");
3238 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3239 if (! a) return 0; // Too early?
3240 a = [a objectAtIndex:row];
3241 NSAssert (a, @"internal error");
3242 return [a objectAtIndex:0];
3245 # endif // USE_PICKER_VIEW
3248 #pragma mark UITableView delegate methods
3250 - (void) addResetButton
3252 [[self navigationItem]
3253 setRightBarButtonItem: [[UIBarButtonItem alloc]
3254 initWithTitle: @"Reset to Defaults"
3255 style: UIBarButtonItemStyleBordered
3257 action:@selector(resetAction:)]];
3258 NSString *s = saver_name;
3259 if ([self view].frame.size.width > 320)
3260 s = [s stringByAppendingString: @" Settings"];
3261 [self navigationItem].title = s;
3265 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3270 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3271 // Number of vertically-stacked white boxes.
3272 return [controls count];
3275 - (NSInteger)tableView:(UITableView *)tableView
3276 numberOfRowsInSection:(NSInteger)section
3278 // Number of lines in each vertically-stacked white box.
3279 NSAssert (controls, @"internal error");
3280 return [[controls objectAtIndex:section] count];
3283 - (NSString *)tableView:(UITableView *)tv
3284 titleForHeaderInSection:(NSInteger)section
3286 // Titles above each vertically-stacked white box.
3287 // if (section == 0)
3288 // return [saver_name stringByAppendingString:@" Settings"];
3293 - (CGFloat)tableView:(UITableView *)tv
3294 heightForRowAtIndexPath:(NSIndexPath *)ip
3298 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3299 objectAtIndex:[ip indexAtPosition:1]];
3301 if ([ctl isKindOfClass:[NSArray class]]) {
3302 NSArray *set = (NSArray *) ctl;
3303 switch ([set count]) {
3304 case 4: // label + left/slider/right.
3305 case 3: // left/slider/right.
3306 h = FONT_SIZE * 3.0;
3308 case 2: // Checkboxes, or text fields.
3309 h = FONT_SIZE * 2.4;
3312 } else if ([ctl isKindOfClass:[UILabel class]]) {
3313 // Radio buttons in a multi-select list.
3314 h = FONT_SIZE * 1.9;
3316 # ifdef USE_HTML_LABELS
3317 } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3319 HTMLLabel *t = (HTMLLabel *) ctl;
3321 r.size.width = [tv frame].size.width;
3322 r.size.width -= LEFT_MARGIN * 2;
3327 # endif // USE_HTML_LABELS
3329 } else { // Does this ever happen?
3330 h = FONT_SIZE + LINE_SPACING * 2;
3333 if (h <= 0) abort();
3338 - (void)refreshTableView
3340 UITableView *tv = (UITableView *) [self view];
3341 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3342 NSInteger rows = [self numberOfSectionsInTableView:tv];
3343 for (int i = 0; i < rows; i++) {
3344 NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3345 for (int j = 0; j < cols; j++) {
3349 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3354 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3357 // Default opacity looks bad.
3358 // #### Oh great, this only works *sometimes*.
3359 UIView *v = [[self navigationItem] titleView];
3360 [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
3364 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3366 [NSTimer scheduledTimerWithTimeInterval: 0
3368 selector:@selector(refreshTableView)
3374 #ifndef USE_PICKER_VIEW
3376 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3377 button:(RadioButton *)b
3379 NSArray *item = [[b items] objectAtIndex: [b index]];
3380 NSString *pref_key = [item objectAtIndex:1];
3381 NSObject *pref_val = [item objectAtIndex:2];
3383 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3385 // Convert them both to strings and compare those, so that
3386 // we don't get screwed by int 1 versus string "1".
3387 // Will boolean true/1 screw us here too?
3389 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3390 ? (NSString *) pref_val
3391 : [(NSNumber *) pref_val stringValue]);
3392 NSString *current_str = ([current isKindOfClass:[NSString class]]
3393 ? (NSString *) current
3394 : [(NSNumber *) current stringValue]);
3395 BOOL match_p = [current_str isEqualToString:pref_str];
3397 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3400 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3402 [cell setAccessoryType:UITableViewCellAccessoryNone];
3406 - (void)tableView:(UITableView *)tv
3407 didSelectRowAtIndexPath:(NSIndexPath *)ip
3409 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3410 objectAtIndex:[ip indexAtPosition:1]];
3411 if (! [ctl isKindOfClass:[RadioButton class]])
3414 [self radioAction:ctl];
3415 [self refreshTableView];
3419 #endif // !USE_PICKER_VIEW
3423 - (UITableViewCell *)tableView:(UITableView *)tv
3424 cellForRowAtIndexPath:(NSIndexPath *)ip
3426 CGFloat ww = [tv frame].size.width;
3427 CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
3429 float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
3431 // Width of the column of labels on the left.
3432 CGFloat left_width = ww * 0.4;
3433 CGFloat right_edge = ww - LEFT_MARGIN;
3435 if (os_version < 7) // margins were wider on iOS 6.1
3438 CGFloat max = FONT_SIZE * 12;
3439 if (left_width > max) left_width = max;
3441 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3442 objectAtIndex:[ip indexAtPosition:1]];
3444 if ([ctl isKindOfClass:[NSArray class]]) {
3445 // This cell has a set of objects in it.
3446 NSArray *set = (NSArray *) ctl;
3447 switch ([set count]) {
3450 // With 2 elements, the first of the pair must be a label.
3451 UILabel *label = (UILabel *) [set objectAtIndex: 0];
3452 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3453 ctl = [set objectAtIndex: 1];
3455 CGRect r = [ctl frame];
3457 if ([ctl isKindOfClass:[UISwitch class]]) { // Checkboxes.
3458 r.size.width = 80; // Magic.
3459 r.origin.x = right_edge - r.size.width + 30; // beats me
3461 if (os_version < 7) // checkboxes were wider on iOS 6.1
3465 r.origin.x = left_width; // Text fields, etc.
3466 r.size.width = right_edge - r.origin.x;
3469 r.origin.y = (hh - r.size.height) / 2; // Center vertically.
3472 // Make a box and put the label and checkbox/slider into it.
3477 NSView *box = [[UIView alloc] initWithFrame:r];
3478 [box addSubview: ctl];
3480 // Let the label make use of any space not taken up by the control.
3482 r.origin.x = LEFT_MARGIN;
3484 r.size.width = [ctl frame].origin.x - r.origin.x;
3487 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3488 [box addSubview: label];
3497 // With 3 elements, 1 and 3 are labels.
3498 // With 4 elements, 1, 2 and 4 are labels.
3500 UILabel *top = ([set count] == 4
3501 ? [set objectAtIndex: i++]
3503 UILabel *left = [set objectAtIndex: i++];
3504 NSView *mid = [set objectAtIndex: i++];
3505 UILabel *right = [set objectAtIndex: i++];
3506 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3507 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3508 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3509 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3511 // 3 elements: control at top of cell.
3512 // 4 elements: center the control vertically.
3513 CGRect r = [mid frame];
3514 r.size.height = 32; // Unchangable height of the slider thumb.
3516 // Center the slider between left_width and right_edge.
3517 # ifdef LABEL_ABOVE_SLIDER
3518 r.origin.x = LEFT_MARGIN;
3520 r.origin.x = left_width;
3522 r.origin.y = (hh - r.size.height) / 2;
3523 r.size.width = right_edge - r.origin.x;
3527 r.size = [[top text] sizeWithFont:[top font]
3529 CGSizeMake (ww - LEFT_MARGIN*2, 100000)
3530 lineBreakMode:[top lineBreakMode]];
3531 # ifdef LABEL_ABOVE_SLIDER
3532 // Top label goes above, flush center/top.
3533 r.origin.x = (ww - r.size.width) / 2;
3535 # else // !LABEL_ABOVE_SLIDER
3536 // Label goes on the left.
3537 r.origin.x = LEFT_MARGIN;
3539 r.size.width = left_width - LEFT_MARGIN;
3541 # endif // !LABEL_ABOVE_SLIDER
3545 // Left label goes under control, flush left/bottom.
3546 r.size = [[left text] sizeWithFont:[left font]
3548 CGSizeMake(ww - LEFT_MARGIN*2, 100000)
3549 lineBreakMode:[left lineBreakMode]];
3550 r.origin.x = [mid frame].origin.x;
3551 r.origin.y = hh - r.size.height - 4;
3554 // Right label goes under control, flush right/bottom.
3556 r.size = [[right text] sizeWithFont:[right font]
3558 CGSizeMake(ww - LEFT_MARGIN*2, 1000000)
3559 lineBreakMode:[right lineBreakMode]];
3560 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3562 r.origin.y = [left frame].origin.y;
3565 // Make a box and put the labels and slider into it.
3570 NSView *box = [[UIView alloc] initWithFrame:r];
3572 [box addSubview: top];
3573 [box addSubview: left];
3574 [box addSubview: right];
3575 [box addSubview: mid];
3582 NSAssert (0, @"unhandled size");
3584 } else { // A single view, not a pair.
3585 CGRect r = [ctl frame];
3586 r.origin.x = LEFT_MARGIN;
3588 r.size.width = right_edge - r.origin.x;
3593 NSString *id = @"Cell";
3594 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
3596 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3597 reuseIdentifier: id]
3600 for (UIView *subview in [cell.contentView subviews])
3601 [subview removeFromSuperview];
3602 [cell.contentView addSubview: ctl];
3603 CGRect r = [ctl frame];
3607 cell.selectionStyle = UITableViewCellSelectionStyleNone;
3608 [cell setAccessoryType:UITableViewCellAccessoryNone];
3610 # ifndef USE_PICKER_VIEW
3611 if ([ctl isKindOfClass:[RadioButton class]])
3612 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3613 # endif // USE_PICKER_VIEW
3617 # endif // USE_IPHONE
3620 /* When this object is instantiated, it parses the XML file and creates
3621 controls on itself that are hooked up to the appropriate preferences.
3622 The default size of the view is just big enough to hold them all.
3624 - (id)initWithXML: (NSData *) xml_data
3625 options: (const XrmOptionDescRec *) _opts
3626 controller: (NSUserDefaultsController *) _prefs
3627 globalController: (NSUserDefaultsController *) _globalPrefs
3628 defaults: (NSDictionary *) _defs
3631 self = [super init];
3632 # else // !USE_IPHONE
3633 self = [super initWithStyle:UITableViewStyleGrouped];
3634 self.title = [saver_name stringByAppendingString:@" Settings"];
3635 # endif // !USE_IPHONE
3636 if (! self) return 0;
3638 // instance variables
3640 defaultOptions = _defs;
3641 userDefaultsController = [_prefs retain];
3642 globalDefaultsController = [_globalPrefs retain];
3644 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3647 NSAssert1 (0, @"XML Error: %@",
3648 [[NSString alloc] initWithData:xml_data
3649 encoding:NSUTF8StringEncoding]);
3652 [xmlDoc setDelegate:self];
3653 if (! [xmlDoc parse]) {
3654 NSError *err = [xmlDoc parserError];
3655 NSAssert2 (0, @"XML Error: %@: %@",
3656 [[NSString alloc] initWithData:xml_data
3657 encoding:NSUTF8StringEncoding],
3663 TextModeTransformer *t = [[TextModeTransformer alloc] init];
3664 [NSValueTransformer setValueTransformer:t
3665 forName:@"TextModeTransformer"];
3667 # endif // USE_IPHONE
3669 [self traverseTree];
3673 [self addResetButton];
3682 [saver_name release];
3683 [userDefaultsController release];
3684 [globalDefaultsController release];
3687 [pref_keys release];
3688 [pref_ctls release];
3689 # ifdef USE_PICKER_VIEW
3690 [picker_values release];