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.
715 [userDefaultsController setAppliesImmediately:YES];
716 [globalDefaultsController setAppliesImmediately:YES];
717 [userDefaultsController commitEditing];
718 [globalDefaultsController commitEditing];
719 [userDefaultsController save:self];
720 [globalDefaultsController save:self];
721 [NSApp endSheet:self returnCode:NSOKButton];
725 - (void) cancelAction:(NSObject *)arg
727 [userDefaultsController revert:self];
728 [globalDefaultsController revert:self];
729 [NSApp endSheet:self returnCode:NSCancelButton];
732 # endif // !USE_IPHONE
735 - (void) resetAction:(NSObject *)arg
738 [userDefaultsController revertToInitialValues:self];
739 [globalDefaultsController revertToInitialValues:self];
742 for (NSString *key in defaultOptions) {
743 NSObject *val = [defaultOptions objectForKey:key];
744 [[self controllerForKey:key] setObject:val forKey:key];
747 for (UIControl *ctl in pref_ctls) {
748 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
749 [self bindResource:ctl key:pref_key reload:YES];
752 [self refreshTableView];
753 # endif // USE_IPHONE
757 /* Connects a control (checkbox, etc) to the corresponding preferences key.
759 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
760 reload:(BOOL)reload_p
762 NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
764 NSDictionary *opts_dict = nil;
765 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
767 : ([control isKindOfClass:[NSMatrix class]]
771 if ([control isKindOfClass:[NSMatrix class]]) {
772 opts_dict = @{ NSValueTransformerNameBindingOption:
773 @"TextModeTransformer" };
778 withKeyPath:[@"values." stringByAppendingString: pref_key]
783 NSObject *val = [prefs objectForKey:pref_key];
787 if ([val isKindOfClass:[NSString class]]) {
788 sval = (NSString *) val;
789 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
790 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
791 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
794 dval = [sval doubleValue];
795 } else if ([val isKindOfClass:[NSNumber class]]) {
796 // NSBoolean (__NSCFBoolean) is really NSNumber.
797 dval = [(NSNumber *) val doubleValue];
798 sval = [(NSNumber *) val stringValue];
801 if ([control isKindOfClass:[UISlider class]]) {
802 sel = @selector(sliderAction:);
803 // Hacky API. See comment in InvertedSlider.m.
804 if ([control isKindOfClass:[InvertedSlider class]])
805 [(InvertedSlider *) control setTransformedValue: dval];
807 [(UISlider *) control setValue: dval];
808 } else if ([control isKindOfClass:[UISwitch class]]) {
809 sel = @selector(switchAction:);
810 [(UISwitch *) control setOn: ((int) dval != 0)];
811 # ifdef USE_PICKER_VIEW
812 } else if ([control isKindOfClass:[UIPickerView class]]) {
814 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
816 # else // !USE_PICKER_VIEW
817 } else if ([control isKindOfClass:[RadioButton class]]) {
818 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
819 } else if ([control isKindOfClass:[UITextField class]]) {
821 [(UITextField *) control setText: sval];
822 # endif // !USE_PICKER_VIEW
824 NSAssert (0, @"unknown class");
827 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
831 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
832 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
835 [pref_keys addObject: [self makeKey:pref_key]];
836 [pref_ctls addObject: control];
837 ((UIControl *) control).tag = [pref_keys count] - 1;
840 [(UIControl *) control addTarget:self action:sel
841 forControlEvents:UIControlEventValueChanged];
845 # endif // USE_IPHONE
848 NSObject *def = [[prefs defaults] objectForKey:pref_key];
849 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
850 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
851 s = [NSString stringWithFormat:@"%@ = %@", s,
852 ([def isKindOfClass:[NSString class]]
853 ? [NSString stringWithFormat:@"\"%@\"", def]
855 s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0];
856 s = [NSString stringWithFormat:@"%@ %@ / %@", s,
857 [def class], [control class]];
859 s = [NSString stringWithFormat:@"%@ / %@", s, bindto];
866 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
868 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
873 - (void) bindSwitch:(NSObject *)control
874 cmdline:(NSString *)cmd
876 [self bindResource:control
877 key:[self switchToResource:cmd opts:opts valRet:0]];
881 #pragma mark Text-manipulating utilities
885 unwrap (NSString *text)
887 // Unwrap lines: delete \n but do not delete \n\n.
889 NSArray *lines = [text componentsSeparatedByString:@"\n"];
890 NSUInteger i, nlines = [lines count];
893 text = @"\n"; // start with one blank line
895 // skip trailing blank lines in file
896 for (i = nlines-1; i > 0; i--) {
897 NSString *s = (NSString *) [lines objectAtIndex:i];
903 // skip leading blank lines in file
904 for (i = 0; i < nlines; i++) {
905 NSString *s = (NSString *) [lines objectAtIndex:i];
912 for (; i < nlines; i++) {
913 NSString *s = (NSString *) [lines objectAtIndex:i];
914 if ([s length] == 0) {
915 text = [text stringByAppendingString:@"\n\n"];
917 } else if ([s characterAtIndex:0] == ' ' ||
918 [s hasPrefix:@"Copyright "] ||
919 [s hasPrefix:@"http://"]) {
920 // don't unwrap if the following line begins with whitespace,
921 // or with the word "Copyright", or if it begins with a URL.
923 text = [text stringByAppendingString:@"\n"];
924 text = [text stringByAppendingString:s];
929 text = [text stringByAppendingString:@" "];
930 text = [text stringByAppendingString:s];
941 /* Makes the text up to the first comma be bold.
944 boldify (NSText *nstext)
946 NSString *text = [nstext string];
947 NSRange r = [text rangeOfString:@"," options:0];
948 r.length = r.location+1;
952 NSFont *font = [nstext font];
953 font = [NSFont boldSystemFontOfSize:[font pointSize]];
954 [nstext setFont:font range:r];
956 # endif // !USE_IPHONE
959 /* Creates a human-readable anchor to put on a URL.
962 anchorize (const char *url)
964 const char *wiki = "http://en.wikipedia.org/wiki/";
965 const char *math = "http://mathworld.wolfram.com/";
966 if (!strncmp (wiki, url, strlen(wiki))) {
967 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
968 strcpy (anchor, "Wikipedia: \"");
969 const char *in = url + strlen(wiki);
970 char *out = anchor + strlen(anchor);
974 } else if (*in == '#') {
977 } else if (*in == '%') {
983 sscanf (hex, "%x", &n);
995 } else if (!strncmp (math, url, strlen(math))) {
996 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
997 strcpy (anchor, "MathWorld: \"");
998 const char *start = url + strlen(wiki);
999 const char *in = start;
1000 char *out = anchor + strlen(anchor);
1004 } else if (in != start && *in >= 'A' && *in <= 'Z') {
1007 } else if (!strncmp (in, ".htm", 4)) {
1019 return strdup (url);
1024 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
1026 /* Converts any http: URLs in the given text field to clickable links.
1029 hreffify (NSText *nstext)
1032 NSString *text = [nstext string];
1033 [nstext setRichText:YES];
1035 NSString *text = [nstext text];
1038 int L = [text length];
1039 NSRange start; // range is start-of-search to end-of-string
1042 while (start.location < L) {
1044 // Find the beginning of a URL...
1046 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1047 if (r2.location == NSNotFound)
1050 // Next time around, start searching after this.
1051 start.location = r2.location + r2.length;
1052 start.length = L - start.location;
1054 // Find the end of a URL (whitespace or EOF)...
1056 NSRange r3 = [text rangeOfCharacterFromSet:
1057 [NSCharacterSet whitespaceAndNewlineCharacterSet]
1058 options:0 range:start];
1059 if (r3.location == NSNotFound) // EOF
1060 r3.location = L, r3.length = 0;
1062 // Next time around, start searching after this.
1063 start.location = r3.location;
1064 start.length = L - start.location;
1066 // Set r2 to the start/length of this URL.
1067 r2.length = start.location - r2.location;
1070 NSString *nsurl = [text substringWithRange:r2];
1071 const char *url = [nsurl UTF8String];
1073 // If this is a Wikipedia URL, make the linked text be prettier.
1075 char *anchor = anchorize(url);
1079 // Construct the RTF corresponding to <A HREF="url">anchor</A>
1081 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1082 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1083 sprintf (rtf, fmt, url, anchor);
1085 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1086 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1088 # else // !USE_IPHONE
1089 // *anchor = 0; // Omit Wikipedia anchor
1090 text = [text stringByReplacingCharactersInRange:r2
1091 withString:[NSString stringWithCString:anchor
1092 encoding:NSUTF8StringEncoding]];
1093 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1094 // withString:@"\n\n"];
1095 # endif // !USE_IPHONE
1099 int L2 = [text length]; // might have changed
1100 start.location -= (L - L2);
1105 [nstext setText:text];
1110 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1114 #pragma mark Creating controls from XML
1117 /* Parse the attributes of an XML tag into a dictionary.
1118 For input, the dictionary should have as attributes the keys, each
1119 with @"" as their value.
1120 On output, the dictionary will set the keys to the values specified,
1121 and keys that were not specified will not be present in the dictionary.
1122 Warnings are printed if there are duplicate or unknown attributes.
1124 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1126 NSArray *attrs = [(NSXMLElement *) node attributes];
1127 NSUInteger n = [attrs count];
1130 // For each key in the dictionary, fill in the dict with the corresponding
1131 // value. The value @"" is assumed to mean "un-set". Issue a warning if
1132 // an attribute is specified twice.
1134 for (i = 0; i < n; i++) {
1135 NSXMLNode *attr = [attrs objectAtIndex:i];
1136 NSString *key = [attr name];
1137 NSString *val = [attr objectValue];
1138 NSString *old = [dict objectForKey:key];
1141 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1142 } else if ([old length] != 0) {
1143 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1145 [dict setValue:val forKey:key];
1149 // Remove from the dictionary any keys whose value is still @"",
1150 // meaning there was no such attribute specified.
1152 NSArray *keys = [dict allKeys];
1154 for (i = 0; i < n; i++) {
1155 NSString *key = [keys objectAtIndex:i];
1156 NSString *val = [dict objectForKey:key];
1157 if ([val length] == 0)
1158 [dict removeObjectForKey:key];
1162 // Kludge for starwars.xml:
1163 // If there is a "_low-label" and no "_label", but "_low-label" contains
1164 // spaces, divide them.
1165 NSString *lab = [dict objectForKey:@"_label"];
1166 NSString *low = [dict objectForKey:@"_low-label"];
1169 [[[low stringByTrimmingCharactersInSet:
1170 [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1171 componentsSeparatedByString: @" "]
1172 filteredArrayUsingPredicate:
1173 [NSPredicate predicateWithFormat:@"length > 0"]];
1174 if (split && [split count] == 2) {
1175 [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1176 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1179 # endif // USE_IPHONE
1183 /* Handle the options on the top level <xscreensaver> tag.
1185 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1187 NSMutableDictionary *dict = [@{ @"name": @"",
1191 [self parseAttrs:dict node:node];
1192 NSString *name = [dict objectForKey:@"name"];
1193 NSString *label = [dict objectForKey:@"_label"];
1197 NSAssert1 (label, @"no _label in %@", [node name]);
1198 NSAssert1 (name, @"no name in \"%@\"", label);
1203 /* Creates a label: an un-editable NSTextField displaying the given text.
1205 - (LABEL *) makeLabel:(NSString *)text
1208 rect.origin.x = rect.origin.y = 0;
1209 rect.size.width = rect.size.height = 10;
1211 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1212 [lab setSelectable:NO];
1213 [lab setEditable:NO];
1214 [lab setBezeled:NO];
1215 [lab setDrawsBackground:NO];
1216 [lab setStringValue:text];
1218 # else // USE_IPHONE
1219 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1220 [lab setText: [text stringByTrimmingCharactersInSet:
1221 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1222 [lab setBackgroundColor:[UIColor clearColor]];
1223 [lab setNumberOfLines:0]; // unlimited
1224 // [lab setLineBreakMode:UILineBreakModeWordWrap];
1225 [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1226 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1227 UIViewAutoresizingFlexibleHeight)];
1228 # endif // USE_IPHONE
1234 /* Creates the checkbox (NSButton) described by the given XML node.
1236 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1238 NSMutableDictionary *dict = [@{ @"id": @"",
1243 [self parseAttrs:dict node:node];
1244 NSString *label = [dict objectForKey:@"_label"];
1245 NSString *arg_set = [dict objectForKey:@"arg-set"];
1246 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1251 NSAssert1 (0, @"no _label in %@", [node name]);
1254 if (!arg_set && !arg_unset) {
1255 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
1258 if (arg_set && arg_unset) {
1259 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
1263 // sanity-check the choice of argument names.
1265 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1266 [arg_set hasPrefix:@"--no-"]))
1267 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1269 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1270 ![arg_unset hasPrefix:@"--no-"]))
1271 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1275 rect.origin.x = rect.origin.y = 0;
1276 rect.size.width = rect.size.height = 10;
1280 NSButton *button = [[NSButton alloc] initWithFrame:rect];
1281 [button setButtonType:NSSwitchButton];
1282 [button setTitle:label];
1284 [self placeChild:button on:parent];
1286 # else // USE_IPHONE
1288 LABEL *lab = [self makeLabel:label];
1289 [self placeChild:lab on:parent];
1290 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1291 [self placeChild:button on:parent right:YES];
1293 # endif // USE_IPHONE
1295 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1300 /* Creates the number selection control described by the given XML node.
1301 If "type=slider", it's an NSSlider.
1302 If "type=spinbutton", it's a text field with up/down arrows next to it.
1304 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1306 NSMutableDictionary *dict = [@{ @"id": @"",
1309 @"_high-label": @"",
1317 [self parseAttrs:dict node:node];
1318 NSString *label = [dict objectForKey:@"_label"];
1319 NSString *low_label = [dict objectForKey:@"_low-label"];
1320 NSString *high_label = [dict objectForKey:@"_high-label"];
1321 NSString *type = [dict objectForKey:@"type"];
1322 NSString *arg = [dict objectForKey:@"arg"];
1323 NSString *low = [dict objectForKey:@"low"];
1324 NSString *high = [dict objectForKey:@"high"];
1325 NSString *def = [dict objectForKey:@"default"];
1326 NSString *cvt = [dict objectForKey:@"convert"];
1330 NSAssert1 (arg, @"no arg in %@", label);
1331 NSAssert1 (type, @"no type in %@", label);
1334 NSAssert1 (0, @"no low in %@", [node name]);
1338 NSAssert1 (0, @"no high in %@", [node name]);
1342 NSAssert1 (0, @"no default in %@", [node name]);
1345 if (cvt && ![cvt isEqualToString:@"invert"]) {
1346 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1350 // If either the min or max field contains a decimal point, then this
1351 // option may have a floating point value; otherwise, it is constrained
1352 // to be an integer.
1354 NSCharacterSet *dot =
1355 [NSCharacterSet characterSetWithCharactersInString:@"."];
1356 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1357 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1359 if ([type isEqualToString:@"slider"]
1360 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1361 || [type isEqualToString:@"spinbutton"]
1366 rect.origin.x = rect.origin.y = 0;
1367 rect.size.width = 150;
1368 rect.size.height = 23; // apparent min height for slider with ticks...
1370 slider = [[InvertedSlider alloc] initWithFrame:rect
1372 integers: !float_p];
1373 [slider setMaxValue:[high doubleValue]];
1374 [slider setMinValue:[low doubleValue]];
1376 int range = [slider maxValue] - [slider minValue] + 1;
1379 while (range2 > max_ticks)
1383 // If we have elided ticks, leave it at the max number of ticks.
1384 if (range != range2 && range2 < max_ticks)
1387 // If it's a float, always display the max number of ticks.
1388 if (float_p && range2 < max_ticks)
1391 [slider setNumberOfTickMarks:range2];
1393 [slider setAllowsTickMarkValuesOnly:
1394 (range == range2 && // we are showing the actual number of ticks
1395 !float_p)]; // and we want integer results
1396 # endif // !USE_IPHONE
1398 // #### Note: when the slider's range is large enough that we aren't
1399 // showing all possible ticks, the slider's value is not constrained
1400 // to be an integer, even though it should be...
1401 // Maybe we need to use a value converter or something?
1405 lab = [self makeLabel:label];
1406 [self placeChild:lab on:parent];
1409 CGFloat s = [NSFont systemFontSize] + 4;
1410 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1416 lab = [self makeLabel:low_label];
1417 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1419 [lab setAlignment:1]; // right aligned
1421 if (rect.size.width < LEFT_LABEL_WIDTH)
1422 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1423 rect.size.height = [slider frame].size.height;
1424 [lab setFrame:rect];
1425 [self placeChild:lab on:parent];
1426 # else // USE_IPHONE
1427 [lab setTextAlignment: NSTextAlignmentRight];
1428 // Sometimes rotation screws up truncation.
1429 [lab setLineBreakMode:NSLineBreakByClipping];
1430 [self placeChild:lab on:parent right:(label ? YES : NO)];
1431 # endif // USE_IPHONE
1435 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1436 # else // USE_IPHONE
1437 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1438 # endif // USE_IPHONE
1441 // Make left label be same height as slider.
1443 rect.size.height = [slider frame].size.height;
1444 [lab setFrame:rect];
1448 rect = [slider frame];
1449 if (rect.origin.x < LEFT_LABEL_WIDTH)
1450 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1451 [slider setFrame:rect];
1455 lab = [self makeLabel:high_label];
1456 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1459 // Make right label be same height as slider.
1460 rect.size.height = [slider frame].size.height;
1461 [lab setFrame:rect];
1463 // Sometimes rotation screws up truncation.
1464 [lab setLineBreakMode:NSLineBreakByClipping];
1466 [self placeChild:lab on:parent right:YES];
1469 [self bindSwitch:slider cmdline:arg];
1472 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1474 } else if ([type isEqualToString:@"spinbutton"]) {
1477 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1480 NSAssert1 (!low_label,
1481 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1482 NSAssert1 (!high_label,
1483 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1484 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1488 rect.origin.x = rect.origin.y = 0;
1489 rect.size.width = rect.size.height = 10;
1491 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1492 [txt setStringValue:@"0000.0"];
1494 [txt setStringValue:@""];
1497 LABEL *lab = [self makeLabel:label];
1498 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1499 [lab setAlignment:1]; // right aligned
1501 if (rect.size.width < LEFT_LABEL_WIDTH)
1502 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1503 rect.size.height = [txt frame].size.height;
1504 [lab setFrame:rect];
1505 [self placeChild:lab on:parent];
1508 [self placeChild:txt on:parent right:(label ? YES : NO)];
1512 if (rect.origin.x < LEFT_LABEL_WIDTH)
1513 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1514 [txt setFrame:rect];
1517 rect.size.width = rect.size.height = 10;
1518 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1520 [self placeChild:step on:parent right:YES];
1521 rect = [step frame];
1522 rect.origin.x -= COLUMN_SPACING; // this one goes close
1523 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1524 [step setFrame:rect];
1526 [step setMinValue:[low doubleValue]];
1527 [step setMaxValue:[high doubleValue]];
1528 [step setAutorepeat:YES];
1529 [step setValueWraps:NO];
1531 double range = [high doubleValue] - [low doubleValue];
1533 [step setIncrement:range / 10.0];
1534 else if (range >= 500)
1535 [step setIncrement:range / 100.0];
1537 [step setIncrement:1.0];
1539 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1540 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1541 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1542 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1543 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1544 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1545 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1547 [fmt setGeneratesDecimalNumbers:float_p];
1548 [[txt cell] setFormatter:fmt];
1550 [self bindSwitch:step cmdline:arg];
1551 [self bindSwitch:txt cmdline:arg];
1556 # endif // USE_IPHONE
1559 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1566 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1568 /* If the object associated with this menu item looks like a boolean,
1569 store an NSNumber instead of an NSString, since that's what
1570 will be in the preferences (due to similar logic in PrefsReader).
1572 if ([obj isKindOfClass:[NSString class]]) {
1573 NSString *string = (NSString *) obj;
1574 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1575 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1576 obj = [NSNumber numberWithBool:YES];
1577 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1578 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1579 obj = [NSNumber numberWithBool:NO];
1584 [item setRepresentedObject:obj];
1585 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1587 # endif // !USE_IPHONE
1590 /* Creates the popup menu described by the given XML node (and its children).
1592 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1594 NSArray *children = [node children];
1595 NSUInteger i, count = [children count];
1598 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1602 // get the "id" attribute off the <select> tag.
1604 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1605 [self parseAttrs:dict node:node];
1610 rect.origin.x = rect.origin.y = 0;
1611 rect.size.width = 10;
1612 rect.size.height = 10;
1614 NSString *menu_key = nil; // the resource key used by items in this menu
1617 // #### "Build and Analyze" says that all of our widgets leak, because it
1618 // seems to not realize that placeChild -> addSubview retains them.
1619 // Not sure what to do to make these warnings go away.
1621 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1623 NSMenuItem *def_item = nil;
1624 float max_width = 0;
1626 # else // USE_IPHONE
1628 NSString *def_item = nil;
1630 rect.size.width = 0;
1631 rect.size.height = 0;
1632 # ifdef USE_PICKER_VIEW
1633 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1634 popup.delegate = self;
1635 popup.dataSource = self;
1636 # endif // !USE_PICKER_VIEW
1637 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1639 # endif // USE_IPHONE
1641 for (i = 0; i < count; i++) {
1642 NSXMLNode *child = [children objectAtIndex:i];
1644 if ([child kind] == NSXMLCommentKind)
1646 if ([child kind] != NSXMLElementKind) {
1647 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1651 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1653 NSMutableDictionary *dict2 = [@{ @"id": @"",
1657 [self parseAttrs:dict2 node:child];
1658 NSString *label = [dict2 objectForKey:@"_label"];
1659 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1664 NSAssert1 (0, @"no _label in %@", [child name]);
1669 // create the menu item (and then get a pointer to it)
1670 [popup addItemWithTitle:label];
1671 NSMenuItem *item = [popup itemWithTitle:label];
1672 # endif // USE_IPHONE
1675 NSString *this_val = NULL;
1676 NSString *this_key = [self switchToResource: arg_set
1679 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1680 if (menu_key && ![menu_key isEqualToString:this_key])
1682 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1683 menu_key, this_key, this_val);
1685 menu_key = this_key;
1687 /* If this menu has the cmd line "-mode foo" then set this item's
1688 value to "foo" (the menu itself will be bound to e.g. "modeString")
1691 set_menu_item_object (item, this_val);
1693 // Array holds ["Label", "resource-key", "resource-val"].
1694 [items addObject:[NSMutableArray arrayWithObjects:
1695 label, @"", this_val, nil]];
1699 // no arg-set -- only one menu item can be missing that.
1700 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1705 // Array holds ["Label", "resource-key", "resource-val"].
1706 [items addObject:[NSMutableArray arrayWithObjects:
1707 label, @"", @"", nil]];
1711 /* make sure the menu button has room for the text of this item,
1712 and remember the greatest width it has reached.
1715 [popup setTitle:label];
1717 NSRect r = [popup frame];
1718 if (r.size.width > max_width) max_width = r.size.width;
1719 # endif // USE_IPHONE
1723 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1727 /* We've added all of the menu items. If there was an item with no
1728 command-line switch, then it's the item that represents the default
1729 value. Now we must bind to that item as well... (We have to bind
1730 this one late, because if it was the first item, then we didn't
1731 yet know what resource was associated with this menu.)
1734 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1736 @"no default value for resource \"%@\" in menu item \"%@\"",
1746 set_menu_item_object (def_item, def_obj);
1747 # else // !USE_IPHONE
1748 for (NSMutableArray *a in items) {
1749 // Make sure each array contains the resource key.
1750 [a replaceObjectAtIndex:1 withObject:menu_key];
1751 // Make sure the default item contains the default resource value.
1752 if (def_obj && def_item &&
1753 [def_item isEqualToString:[a objectAtIndex:0]])
1754 [a replaceObjectAtIndex:2 withObject:def_obj];
1756 # endif // !USE_IPHONE
1760 # ifdef USE_PICKER_VIEW
1761 /* Finish tweaking the menu button itself.
1764 [popup setTitle:[def_item title]];
1765 NSRect r = [popup frame];
1766 r.size.width = max_width;
1768 # endif // USE_PICKER_VIEW
1771 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1772 [self placeChild:popup on:parent];
1773 [self bindResource:popup key:menu_key];
1778 # ifdef USE_PICKER_VIEW
1779 // Store the items for this picker in the picker_values array.
1780 // This is so fucking stupid.
1782 unsigned long menu_number = [pref_keys count] - 1;
1783 if (! picker_values)
1784 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1785 while ([picker_values count] <= menu_number)
1786 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1787 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1788 [popup reloadAllComponents];
1790 # else // !USE_PICKER_VIEW
1792 [self placeSeparator];
1795 for (__attribute__((unused)) NSArray *item in items) {
1796 RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1798 [b setLineBreakMode:NSLineBreakByTruncatingHead];
1799 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1800 [self placeChild:b on:parent];
1805 [self placeSeparator];
1807 # endif // !USE_PICKER_VIEW
1808 # endif // !USE_IPHONE
1813 /* Creates an uneditable, wrapping NSTextField to display the given
1814 text enclosed by <description> ... </description> in the XML.
1816 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1818 NSString *text = nil;
1819 NSArray *children = [node children];
1820 NSUInteger i, count = [children count];
1822 for (i = 0; i < count; i++) {
1823 NSXMLNode *child = [children objectAtIndex:i];
1824 NSString *s = [child objectValue];
1826 text = [text stringByAppendingString:s];
1831 text = unwrap (text);
1833 NSRect rect = [parent frame];
1834 rect.origin.x = rect.origin.y = 0;
1835 rect.size.width = 200;
1836 rect.size.height = 50; // sized later
1838 NSText *lab = [[NSText alloc] initWithFrame:rect];
1840 [lab setEditable:NO];
1841 [lab setDrawsBackground:NO];
1842 [lab setHorizontallyResizable:YES];
1843 [lab setVerticallyResizable:YES];
1844 [lab setString:text];
1849 # else // USE_IPHONE
1851 # ifndef USE_HTML_LABELS
1853 UILabel *lab = [self makeLabel:text];
1854 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1857 # else // USE_HTML_LABELS
1858 HTMLLabel *lab = [[HTMLLabel alloc]
1860 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1862 [lab setFrame:rect];
1864 # endif // USE_HTML_LABELS
1866 [self placeSeparator];
1868 # endif // USE_IPHONE
1870 [self placeChild:lab on:parent];
1874 /* Creates the NSTextField described by the given XML node.
1876 - (void) makeTextField: (NSXMLNode *)node
1877 on: (NSView *)parent
1878 withLabel: (BOOL) label_p
1879 horizontal: (BOOL) horiz_p
1881 NSMutableDictionary *dict = [@{ @"id": @"",
1885 [self parseAttrs:dict node:node];
1886 NSString *label = [dict objectForKey:@"_label"];
1887 NSString *arg = [dict objectForKey:@"arg"];
1891 if (!label && label_p) {
1892 NSAssert1 (0, @"no _label in %@", [node name]);
1896 NSAssert1 (arg, @"no arg in %@", label);
1899 rect.origin.x = rect.origin.y = 0;
1900 rect.size.width = rect.size.height = 10;
1902 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1906 // make the default size be around 30 columns; a typical value for
1907 // these text fields is "xscreensaver-text --cols 40".
1909 [txt setStringValue:@"123456789 123456789 123456789 "];
1911 [[txt cell] setWraps:NO];
1912 [[txt cell] setScrollable:YES];
1913 [txt setStringValue:@""];
1915 # else // USE_IPHONE
1917 txt.adjustsFontSizeToFitWidth = YES;
1918 txt.textColor = [UIColor blackColor];
1919 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1920 txt.placeholder = @"";
1921 txt.borderStyle = UITextBorderStyleRoundedRect;
1922 txt.textAlignment = NSTextAlignmentRight;
1923 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1924 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1925 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1926 txt.clearButtonMode = UITextFieldViewModeAlways;
1927 txt.returnKeyType = UIReturnKeyDone;
1928 txt.delegate = self;
1930 [txt setEnabled: YES];
1932 rect.size.height = [txt.font lineHeight] * 1.2;
1933 [txt setFrame:rect];
1935 # endif // USE_IPHONE
1938 LABEL *lab = [self makeLabel:label];
1939 [self placeChild:lab on:parent];
1942 [self placeChild:txt on:parent right:(label ? YES : NO)];
1944 [self bindSwitch:txt cmdline:arg];
1949 /* Creates the NSTextField described by the given XML node,
1950 and hooks it up to a Choose button and a file selector widget.
1952 - (void) makeFileSelector: (NSXMLNode *)node
1953 on: (NSView *)parent
1954 dirsOnly: (BOOL) dirsOnly
1955 withLabel: (BOOL) label_p
1956 editable: (BOOL) editable_p
1958 # ifndef USE_IPHONE // No files. No selectors.
1959 NSMutableDictionary *dict = [@{ @"id": @"",
1963 [self parseAttrs:dict node:node];
1964 NSString *label = [dict objectForKey:@"_label"];
1965 NSString *arg = [dict objectForKey:@"arg"];
1969 if (!label && label_p) {
1970 NSAssert1 (0, @"no _label in %@", [node name]);
1974 NSAssert1 (arg, @"no arg in %@", label);
1977 rect.origin.x = rect.origin.y = 0;
1978 rect.size.width = rect.size.height = 10;
1980 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1982 // make the default size be around 20 columns.
1984 [txt setStringValue:@"123456789 123456789 "];
1986 [txt setSelectable:YES];
1987 [txt setEditable:editable_p];
1988 [txt setBezeled:editable_p];
1989 [txt setDrawsBackground:editable_p];
1990 [[txt cell] setWraps:NO];
1991 [[txt cell] setScrollable:YES];
1992 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1993 [txt setStringValue:@""];
1997 lab = [self makeLabel:label];
1998 [self placeChild:lab on:parent];
2001 [self placeChild:txt on:parent right:(label ? YES : NO)];
2003 [self bindSwitch:txt cmdline:arg];
2006 // Make the text field and label be the same height, whichever is taller.
2009 rect.size.height = ([lab frame].size.height > [txt frame].size.height
2010 ? [lab frame].size.height
2011 : [txt frame].size.height);
2012 [txt setFrame:rect];
2015 // Now put a "Choose" button next to it.
2017 rect.origin.x = rect.origin.y = 0;
2018 rect.size.width = rect.size.height = 10;
2019 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
2020 [choose setTitle:@"Choose..."];
2021 [choose setBezelStyle:NSRoundedBezelStyle];
2024 [self placeChild:choose on:parent right:YES];
2026 // center the Choose button around the midpoint of the text field.
2027 rect = [choose frame];
2028 rect.origin.y = ([txt frame].origin.y +
2029 (([txt frame].size.height - rect.size.height) / 2));
2030 [choose setFrameOrigin:rect.origin];
2032 [choose setTarget:[parent window]];
2034 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
2036 [choose setAction:@selector(fileSelectorChooseAction:)];
2039 # endif // !USE_IPHONE
2045 /* Runs a modal file selector and sets the text field's value to the
2046 selected file or directory.
2049 do_file_selector (NSTextField *txt, BOOL dirs_p)
2051 NSOpenPanel *panel = [NSOpenPanel openPanel];
2052 [panel setAllowsMultipleSelection:NO];
2053 [panel setCanChooseFiles:!dirs_p];
2054 [panel setCanChooseDirectories:dirs_p];
2056 int result = [panel runModal];
2057 if (result == NSOKButton) {
2058 NSArray *files = [panel URLs];
2059 NSString *file = ([files count] > 0 ? [[files objectAtIndex:0] path] : @"");
2060 file = [file stringByAbbreviatingWithTildeInPath];
2061 [txt setStringValue:file];
2063 // Fuck me! Just setting the value of the NSTextField does not cause
2064 // that to end up in the preferences!
2066 NSDictionary *dict = [txt infoForBinding:@"value"];
2067 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
2068 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
2069 if ([path hasPrefix:@"values."]) // WTF.
2070 path = [path substringFromIndex:7];
2071 [[prefs values] setValue:file forKey:path];
2076 /* Returns the NSTextField that is to the left of or above the NSButton.
2078 static NSTextField *
2079 find_text_field_of_button (NSButton *button)
2081 NSView *parent = [button superview];
2082 NSArray *kids = [parent subviews];
2083 int nkids = [kids count];
2086 for (i = 0; i < nkids; i++) {
2087 NSObject *kid = [kids objectAtIndex:i];
2088 if ([kid isKindOfClass:[NSTextField class]]) {
2089 f = (NSTextField *) kid;
2090 } else if (kid == button) {
2099 - (void) fileSelectorChooseAction:(NSObject *)arg
2101 NSButton *choose = (NSButton *) arg;
2102 NSTextField *txt = find_text_field_of_button (choose);
2103 do_file_selector (txt, NO);
2106 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2108 NSButton *choose = (NSButton *) arg;
2109 NSTextField *txt = find_text_field_of_button (choose);
2110 do_file_selector (txt, YES);
2113 #endif // !USE_IPHONE
2116 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2121 (x) Computer name and time
2122 ( ) Text [__________________________]
2123 ( ) Text file [_________________] [Choose]
2124 ( ) URL [__________________________]
2125 ( ) Shell Cmd [__________________________]
2127 textMode -text-mode date
2128 textMode -text-mode literal textLiteral -text-literal %
2129 textMode -text-mode file textFile -text-file %
2130 textMode -text-mode url textURL -text-url %
2131 textMode -text-mode program textProgram -text-program %
2134 rect.size.width = rect.size.height = 1;
2135 rect.origin.x = rect.origin.y = 0;
2136 NSView *group = [[NSView alloc] initWithFrame:rect];
2137 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2139 Bool program_p = TRUE;
2144 // This is how you link radio buttons together.
2146 NSButtonCell *proto = [[NSButtonCell alloc] init];
2147 [proto setButtonType:NSRadioButton];
2149 rect.origin.x = rect.origin.y = 0;
2150 rect.size.width = rect.size.height = 10;
2151 NSMatrix *matrix = [[NSMatrix alloc]
2153 mode:NSRadioModeMatrix
2155 numberOfRows: 4 + (program_p ? 1 : 0)
2157 [matrix setAllowsEmptySelection:NO];
2159 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
2160 [cnames addObject:@"Computer name and time"];
2161 [cnames addObject:@"Text"];
2162 [cnames addObject:@"File"];
2163 [cnames addObject:@"URL"];
2164 if (program_p) [cnames addObject:@"Shell Cmd"];
2165 [matrix bind:@"content"
2167 withKeyPath:@"arrangedObjects"
2171 [self bindSwitch:matrix cmdline:@"-text-mode %"];
2173 [self placeChild:matrix on:group];
2174 [self placeChild:rgroup on:group right:YES];
2181 # else // USE_IPHONE
2183 NSView *rgroup = parent;
2186 // <select id="textMode">
2187 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
2188 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
2189 // <option id="url" _label="Display URL"/>
2192 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2193 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2195 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2196 [node3 setAttributesAsDictionary:
2198 @"arg-set": @"-text-mode date",
2199 @"_label": @"Display the date and time" }];
2200 [node3 setParent: node2];
2201 [node3 autorelease];
2203 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2204 [node3 setAttributesAsDictionary:
2206 @"arg-set": @"-text-mode literal",
2207 @"_label": @"Display static text" }];
2208 [node3 setParent: node2];
2209 [node3 autorelease];
2211 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2212 [node3 setAttributesAsDictionary:
2214 @"_label": @"Display the contents of a URL" }];
2215 [node3 setParent: node2];
2216 [node3 autorelease];
2218 [self makeOptionMenu:node2 on:rgroup];
2221 # endif // USE_IPHONE
2224 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2225 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2226 [node2 setAttributesAsDictionary:
2227 @{ @"id": @"textLiteral",
2228 @"arg": @"-text-literal %",
2230 @"_label": @"Text to display"
2233 [self makeTextField:node2 on:rgroup
2242 // rect = [last_child(rgroup) frame];
2244 /* // trying to make the text fields be enabled only when the checkbox is on..
2245 control = last_child (rgroup);
2246 [control bind:@"enabled"
2247 toObject:[matrix cellAtRow:1 column:0]
2248 withKeyPath:@"value"
2254 // <file id="textFile" _label="" arg-set="-text-file %"/>
2255 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2256 [node2 setAttributesAsDictionary:
2257 @{ @"id": @"textFile",
2258 @"arg": @"-text-file %" }];
2259 [self makeFileSelector:node2 on:rgroup
2260 dirsOnly:NO withLabel:NO editable:NO];
2262 # endif // !USE_IPHONE
2264 // rect = [last_child(rgroup) frame];
2266 // <string id="textURL" _label="" arg-set="text-url %"/>
2267 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2268 [node2 setAttributesAsDictionary:
2269 @{ @"id": @"textURL",
2270 @"arg": @"-text-url %",
2272 @"_label": @"URL to display",
2275 [self makeTextField:node2 on:rgroup
2284 // rect = [last_child(rgroup) frame];
2288 // <string id="textProgram" _label="" arg-set="text-program %"/>
2289 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2290 [node2 setAttributesAsDictionary:
2291 @{ @"id": @"textProgram",
2292 @"arg": @"-text-program %",
2294 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2298 // rect = [last_child(rgroup) frame];
2300 layout_group (rgroup, NO);
2302 rect = [rgroup frame];
2303 rect.size.width += 35; // WTF? Why is rgroup too narrow?
2304 [rgroup setFrame:rect];
2307 // Set the height of the cells in the radio-box matrix to the height of
2308 // the (last of the) text fields.
2309 control = last_child (rgroup);
2310 rect = [control frame];
2311 rect.size.width = 30; // width of the string "Text", plus a bit...
2313 rect.size.width += 25;
2314 rect.size.height += LINE_SPACING;
2315 [matrix setCellSize:rect.size];
2316 [matrix sizeToCells];
2318 layout_group (group, YES);
2319 rect = [matrix frame];
2320 rect.origin.x += rect.size.width + COLUMN_SPACING;
2321 rect.origin.y -= [control frame].size.height - LINE_SPACING;
2322 [rgroup setFrameOrigin:rect.origin];
2324 // now cheat on the size of the matrix: allow it to overlap (underlap)
2327 rect.size = [matrix cellSize];
2328 rect.size.width = 300;
2329 [matrix setCellSize:rect.size];
2330 [matrix sizeToCells];
2332 // Cheat on the position of the stuff on the right (the rgroup).
2333 // GAAAH, this code is such crap!
2334 rect = [rgroup frame];
2336 [rgroup setFrame:rect];
2339 rect.size.width = rect.size.height = 0;
2340 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2341 [box setTitlePosition:NSAtTop];
2342 [box setBorderType:NSBezelBorder];
2343 [box setTitle:@"Display Text"];
2345 rect.size.width = rect.size.height = 12;
2346 [box setContentViewMargins:rect.size];
2347 [box setContentView:group];
2350 [self placeChild:box on:parent];
2354 # endif // !USE_IPHONE
2358 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2361 [x] Grab desktop images
2362 [ ] Choose random image:
2363 [__________________________] [Choose]
2365 <boolean id="grabDesktopImages" _label="Grab desktop images"
2366 arg-unset="-no-grab-desktop"/>
2367 <boolean id="chooseRandomImages" _label="Grab desktop images"
2368 arg-unset="-choose-random-images"/>
2369 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2372 NSXMLElement *node2;
2375 # define SCREENS "Grab desktop images"
2376 # define PHOTOS "Choose random images"
2378 # define SCREENS "Grab screenshots"
2379 # define PHOTOS "Use photo library"
2382 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2383 [node2 setAttributesAsDictionary:
2384 @{ @"id": @"grabDesktopImages",
2385 @"_label": @ SCREENS,
2386 @"arg-unset": @"-no-grab-desktop",
2388 [self makeCheckbox:node2 on:parent];
2391 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2392 [node2 setAttributesAsDictionary:
2393 @{ @"id": @"chooseRandomImages",
2394 @"_label": @ PHOTOS,
2395 @"arg-set": @"-choose-random-images",
2397 [self makeCheckbox:node2 on:parent];
2400 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2401 [node2 setAttributesAsDictionary:
2402 @{ @"id": @"imageDirectory",
2403 @"_label": @"Images from:",
2404 @"arg": @"-image-directory %",
2406 [self makeFileSelector:node2 on:parent
2407 dirsOnly:YES withLabel:YES editable:YES];
2414 // Add a second, explanatory label below the file/URL selector.
2417 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2418 [self placeChild:lab2 on:parent];
2420 // Pack it in a little tighter vertically.
2421 NSRect r2 = [lab2 frame];
2424 [lab2 setFrameOrigin:r2.origin];
2425 # endif // USE_IPHONE
2429 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2433 [x] Check for Updates [ Monthly ]
2436 <boolean id="automaticallyChecksForUpdates"
2437 _label="Automatically check for updates"
2438 arg-unset="-no-automaticallyChecksForUpdates" />
2439 <select id="updateCheckInterval">
2440 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2441 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/>
2442 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2443 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2451 rect.size.width = rect.size.height = 1;
2452 rect.origin.x = rect.origin.y = 0;
2453 NSView *group = [[NSView alloc] initWithFrame:rect];
2455 NSXMLElement *node2;
2459 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2460 [node2 setAttributesAsDictionary:
2461 @{ @"id": @SUSUEnableAutomaticChecksKey,
2462 @"_label": @"Automatically check for updates",
2463 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2465 [self makeCheckbox:node2 on:group];
2470 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2471 [node2 setAttributesAsDictionary:
2472 @{ @"id": @SUScheduledCheckIntervalKey }];
2476 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2477 [node3 setAttributesAsDictionary:
2478 @{ @"id": @"hourly",
2479 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2480 @"_label": @"Hourly" }];
2481 [node3 setParent: node2];
2482 [node3 autorelease];
2484 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2485 [node3 setAttributesAsDictionary:
2487 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2488 @"_label": @"Daily" }];
2489 [node3 setParent: node2];
2490 [node3 autorelease];
2492 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2493 [node3 setAttributesAsDictionary:
2494 @{ @"id": @"weekly",
2495 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2496 @"_label": @"Weekly",
2498 [node3 setParent: node2];
2499 [node3 autorelease];
2501 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2502 [node3 setAttributesAsDictionary:
2503 @{ @"id": @"monthly",
2504 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2505 @"_label": @"Monthly",
2507 [node3 setParent: node2];
2508 [node3 autorelease];
2511 [self makeOptionMenu:node2 on:group];
2515 layout_group (group, TRUE);
2517 rect.size.width = rect.size.height = 0;
2518 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2519 [box setTitlePosition:NSNoTitle];
2520 [box setBorderType:NSNoBorder];
2521 [box setContentViewMargins:rect.size];
2522 [box setContentView:group];
2525 [self placeChild:box on:parent];
2530 # endif // !USE_IPHONE
2534 #pragma mark Layout for controls
2539 last_child (NSView *parent)
2541 NSArray *kids = [parent subviews];
2542 int nkids = [kids count];
2546 return [kids objectAtIndex:nkids-1];
2548 #endif // USE_IPHONE
2551 /* Add the child as a subview of the parent, positioning it immediately
2552 below or to the right of the previously-added child of that view.
2554 - (void) placeChild:
2560 on:(NSView *)parent right:(BOOL)right_p
2563 NSRect rect = [child frame];
2564 NSView *last = last_child (parent);
2566 rect.origin.x = LEFT_MARGIN;
2567 rect.origin.y = ([parent frame].size.height - rect.size.height
2569 } else if (right_p) {
2570 rect = [last frame];
2571 rect.origin.x += rect.size.width + COLUMN_SPACING;
2573 rect = [last frame];
2574 rect.origin.x = LEFT_MARGIN;
2575 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2577 NSRect r = [child frame];
2578 r.origin = rect.origin;
2580 [parent addSubview:child];
2582 # else // USE_IPHONE
2584 /* Controls is an array of arrays of the controls, divided into sections.
2585 Each hgroup / vgroup gets a nested array, too, e.g.:
2587 [ [ [ <label>, <checkbox> ],
2588 [ <label>, <checkbox> ],
2589 [ <label>, <checkbox> ] ],
2590 [ <label>, <text-field> ],
2591 [ <label>, <low-label>, <slider>, <high-label> ],
2592 [ <low-label>, <slider>, <high-label> ],
2596 If an element begins with a label, it is terminal, otherwise it is a
2597 group. There are (currently) never more than 4 elements in a single
2600 A blank vertical spacer is placed between each hgroup / vgroup,
2601 by making each of those a new section in the TableView.
2604 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2605 if ([controls count] == 0)
2606 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2607 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2609 if (!right_p || [current count] == 0) {
2610 // Nothing on the current line. Add this object.
2611 [current addObject: child];
2613 // Something's on the current line already.
2614 NSObject *old = [current objectAtIndex:[current count]-1];
2615 if ([old isKindOfClass:[NSMutableArray class]]) {
2616 // Already an array in this cell. Append.
2617 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2618 [(NSMutableArray *) old addObject: child];
2620 // Replace the control in this cell with an array, then append
2621 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2622 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2625 # endif // USE_IPHONE
2629 - (void) placeChild:(NSView *)child on:(NSView *)parent
2631 [self placeChild:child on:parent right:NO];
2637 // Start putting subsequent children in a new group, to create a new
2638 // section on the UITableView.
2640 - (void) placeSeparator
2642 if (! controls) return;
2643 if ([controls count] == 0) return;
2644 if ([[controls objectAtIndex:[controls count]-1]
2646 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2648 #endif // USE_IPHONE
2652 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2653 wrapped in <hgroup> or <vgroup> in the XML.
2655 - (void) makeGroup:(NSXMLNode *)node
2657 horizontal:(BOOL) horiz_p
2660 if (!horiz_p) [self placeSeparator];
2661 [self traverseChildren:node on:parent];
2662 if (!horiz_p) [self placeSeparator];
2663 # else // !USE_IPHONE
2665 rect.size.width = rect.size.height = 1;
2666 rect.origin.x = rect.origin.y = 0;
2667 NSView *group = [[NSView alloc] initWithFrame:rect];
2668 [self traverseChildren:node on:group];
2670 layout_group (group, horiz_p);
2672 rect.size.width = rect.size.height = 0;
2673 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2674 [box setTitlePosition:NSNoTitle];
2675 [box setBorderType:NSNoBorder];
2676 [box setContentViewMargins:rect.size];
2677 [box setContentView:group];
2680 [self placeChild:box on:parent];
2683 # endif // !USE_IPHONE
2689 layout_group (NSView *group, BOOL horiz_p)
2691 NSArray *kids = [group subviews];
2692 int nkids = [kids count];
2694 double maxx = 0, miny = 0;
2695 for (i = 0; i < nkids; i++) {
2696 NSView *kid = [kids objectAtIndex:i];
2697 NSRect r = [kid frame];
2700 maxx += r.size.width + COLUMN_SPACING;
2701 if (r.size.height > -miny) miny = -r.size.height;
2703 if (r.size.width > maxx) maxx = r.size.width;
2704 miny = r.origin.y - r.size.height;
2711 rect.size.width = maxx;
2712 rect.size.height = -miny;
2713 [group setFrame:rect];
2716 for (i = 0; i < nkids; i++) {
2717 NSView *kid = [kids objectAtIndex:i];
2718 NSRect r = [kid frame];
2720 r.origin.y = rect.size.height - r.size.height;
2722 x += r.size.width + COLUMN_SPACING;
2729 #endif // !USE_IPHONE
2732 /* Create some kind of control corresponding to the given XML node.
2734 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2736 NSString *name = [node name];
2738 if ([node kind] == NSXMLCommentKind)
2741 if ([node kind] == NSXMLTextKind) {
2742 NSString *s = [(NSString *) [node objectValue]
2743 stringByTrimmingCharactersInSet:
2744 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2745 if (! [s isEqualToString:@""]) {
2746 NSAssert1 (0, @"unexpected text: %@", s);
2751 if ([node kind] != NSXMLElementKind) {
2752 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2756 if ([name isEqualToString:@"hgroup"] ||
2757 [name isEqualToString:@"vgroup"]) {
2759 [self makeGroup:node on:parent
2760 horizontal:[name isEqualToString:@"hgroup"]];
2762 } else if ([name isEqualToString:@"command"]) {
2763 // do nothing: this is the "-root" business
2765 } else if ([name isEqualToString:@"video"]) {
2768 } else if ([name isEqualToString:@"boolean"]) {
2769 [self makeCheckbox:node on:parent];
2771 } else if ([name isEqualToString:@"string"]) {
2772 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2774 } else if ([name isEqualToString:@"file"]) {
2775 [self makeFileSelector:node on:parent
2776 dirsOnly:NO withLabel:YES editable:NO];
2778 } else if ([name isEqualToString:@"number"]) {
2779 [self makeNumberSelector:node on:parent];
2781 } else if ([name isEqualToString:@"select"]) {
2782 [self makeOptionMenu:node on:parent];
2784 } else if ([name isEqualToString:@"_description"]) {
2785 [self makeDescLabel:node on:parent];
2787 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2788 [self makeTextLoaderControlBox:node on:parent];
2790 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2791 [self makeImageLoaderControlBox:node on:parent];
2793 } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2794 [self makeUpdaterControlBox:node on:parent];
2797 NSAssert1 (0, @"unknown tag: %@", name);
2802 /* Iterate over and process the children of this XML node.
2804 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2806 NSArray *children = [node children];
2807 NSUInteger i, count = [children count];
2808 for (i = 0; i < count; i++) {
2809 NSXMLNode *child = [children objectAtIndex:i];
2810 [self makeControl:child on:parent];
2817 /* Kludgey magic to make the window enclose the controls we created.
2820 fix_contentview_size (NSView *parent)
2823 NSArray *kids = [parent subviews];
2824 int nkids = [kids count];
2825 NSView *text = 0; // the NSText at the bottom of the window
2826 double maxx = 0, miny = 0;
2829 /* Find the size of the rectangle taken up by each of the children
2830 except the final "NSText" child.
2832 for (i = 0; i < nkids; i++) {
2833 NSView *kid = [kids objectAtIndex:i];
2834 if ([kid isKindOfClass:[NSText class]]) {
2839 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2840 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2841 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2842 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2843 // f.origin.y + f.size.height, [kid class]);
2846 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2848 /* Now that we know the width of the window, set the width of the NSText to
2849 that, so that it can decide what its height needs to be.
2851 if (! text) abort();
2853 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2854 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2855 // f.origin.y + f.size.height, [text class]);
2857 // set the NSText's width (this changes its height).
2858 f.size.width = maxx - LEFT_MARGIN;
2861 // position the NSText below the last child (this gives us a new miny).
2863 f.origin.y = miny - f.size.height - LINE_SPACING;
2864 miny = f.origin.y - LINE_SPACING;
2867 // Lock the width of the field and unlock the height, and let it resize
2868 // once more, to compute the proper height of the text for that width.
2870 [(NSText *) text setHorizontallyResizable:NO];
2871 [(NSText *) text setVerticallyResizable:YES];
2872 [(NSText *) text sizeToFit];
2874 // Now lock the height too: no more resizing this text field.
2876 [(NSText *) text setVerticallyResizable:NO];
2878 // Now reposition the top edge of the text field to be back where it
2879 // was before we changed the height.
2881 float oh = f.size.height;
2883 float dh = f.size.height - oh;
2886 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2887 // If we do this in 10.6, the text field moves down, off the window.
2888 // So instead we repair it at the end, at the "WTF2" comment.
2891 // Also adjust the parent height by the change in height of the text field.
2894 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2895 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2896 // f.origin.y + f.size.height, [text class]);
2899 /* Set the contentView to the size of the children.
2902 // float yoff = f.size.height;
2903 f.size.width = maxx + LEFT_MARGIN;
2904 f.size.height = -(miny - LEFT_MARGIN*2);
2905 // yoff = f.size.height - yoff;
2906 [parent setFrame:f];
2908 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2909 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2911 /* Now move all of the kids up into the window.
2914 float shift = f.size.height;
2915 // NSLog(@"shift: %3.0f", shift);
2916 for (i = 0; i < nkids; i++) {
2917 NSView *kid = [kids objectAtIndex:i];
2919 f.origin.y += shift;
2921 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2922 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2923 // f.origin.y + f.size.height, [kid class]);
2928 parent: 420 x 541 @ 0 0
2929 text: 380 x 100 @ 20 22 miny=-501
2932 parent: 420 x 541 @ 0 0
2933 text: 380 x 100 @ 20 50 miny=-501
2936 // #### WTF2: See "WTF" above. If the text field is off the screen,
2937 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2940 if (f.origin.y < 50) { // magic numbers, yay
2945 /* Set the kids to track the top left corner of the window when resized.
2946 Set the NSText to track the bottom right corner as well.
2948 for (i = 0; i < nkids; i++) {
2949 NSView *kid = [kids objectAtIndex:i];
2950 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2951 if ([kid isKindOfClass:[NSText class]])
2952 mask |= NSViewWidthSizable|NSViewHeightSizable;
2953 [kid setAutoresizingMask:mask];
2956 # endif // !USE_IPHONE
2962 wrap_with_buttons (NSWindow *window, NSView *panel)
2966 // Make a box to hold the buttons at the bottom of the window.
2968 rect = [panel frame];
2969 rect.origin.x = rect.origin.y = 0;
2970 rect.size.height = 10;
2971 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2972 [bbox setTitlePosition:NSNoTitle];
2973 [bbox setBorderType:NSNoBorder];
2975 // Make some buttons: Default, Cancel, OK
2977 rect.origin.x = rect.origin.y = 0;
2978 rect.size.width = rect.size.height = 10;
2979 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2980 [reset setTitle:@"Reset to Defaults"];
2981 [reset setBezelStyle:NSRoundedBezelStyle];
2984 rect = [reset frame];
2985 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2986 [ok setTitle:@"OK"];
2987 [ok setBezelStyle:NSRoundedBezelStyle];
2989 rect = [bbox frame];
2990 rect.origin.x = rect.size.width - [ok frame].size.width;
2991 [ok setFrameOrigin:rect.origin];
2994 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2995 [cancel setTitle:@"Cancel"];
2996 [cancel setBezelStyle:NSRoundedBezelStyle];
2998 rect.origin.x -= [cancel frame].size.width + 10;
2999 [cancel setFrameOrigin:rect.origin];
3001 // Bind OK to RET and Cancel to ESC.
3002 [ok setKeyEquivalent:@"\r"];
3003 [cancel setKeyEquivalent:@"\e"];
3005 // The correct width for OK and Cancel buttons is 68 pixels
3006 // ("Human Interface Guidelines: Controls: Buttons:
3007 // Push Button Specifications").
3010 rect.size.width = 68;
3013 rect = [cancel frame];
3014 rect.size.width = 68;
3015 [cancel setFrame:rect];
3017 // It puts the buttons in the box or else it gets the hose again
3019 [bbox addSubview:ok];
3020 [bbox addSubview:cancel];
3021 [bbox addSubview:reset];
3024 // make a box to hold the button-box, and the preferences view
3026 rect = [bbox frame];
3027 rect.origin.y += rect.size.height;
3028 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
3029 [pbox setTitlePosition:NSNoTitle];
3030 [pbox setBorderType:NSBezelBorder];
3032 // Enforce a max height on the dialog, so that it's obvious to me
3033 // (on a big screen) when the dialog will fall off the bottom of
3034 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
3036 NSRect f = [panel frame];
3037 int screen_height = (768 // shortest "modern" Mac display
3039 - 56 // System Preferences toolbar
3040 - 140 // default magnified bottom dock icon
3042 if (f.size.height > screen_height) {
3043 NSLog(@"%@ height was %.0f; clipping to %d",
3044 [panel class], f.size.height, screen_height);
3045 f.size.height = screen_height;
3050 [pbox addSubview:panel];
3051 [pbox addSubview:bbox];
3054 [reset setAutoresizingMask:NSViewMaxXMargin];
3055 [cancel setAutoresizingMask:NSViewMinXMargin];
3056 [ok setAutoresizingMask:NSViewMinXMargin];
3057 [bbox setAutoresizingMask:NSViewWidthSizable];
3061 [ok setTarget:window];
3062 [cancel setTarget:window];
3063 [reset setTarget:window];
3064 [ok setAction:@selector(okAction:)];
3065 [cancel setAction:@selector(cancelAction:)];
3066 [reset setAction:@selector(resetAction:)];
3072 #endif // !USE_IPHONE
3075 /* Iterate over and process the children of the root node of the XML document.
3077 - (void)traverseTree
3080 NSView *parent = [self view];
3082 NSWindow *parent = self;
3084 NSXMLNode *node = xml_root;
3086 if (![[node name] isEqualToString:@"screensaver"]) {
3087 NSAssert (0, @"top level node is not <xscreensaver>");
3090 saver_name = [self parseXScreenSaverTag: node];
3091 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
3093 [saver_name retain];
3098 rect.origin.x = rect.origin.y = 0;
3099 rect.size.width = rect.size.height = 1;
3101 NSView *panel = [[NSView alloc] initWithFrame:rect];
3102 [self traverseChildren:node on:panel];
3103 fix_contentview_size (panel);
3105 NSView *root = wrap_with_buttons (parent, panel);
3106 [userDefaultsController setAppliesImmediately:NO];
3107 [globalDefaultsController setAppliesImmediately:NO];
3109 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
3111 rect = [parent frameRectForContentRect:[root frame]];
3112 [parent setFrame:rect display:NO];
3113 [parent setMinSize:rect.size];
3115 [parent setContentView:root];
3120 # else // USE_IPHONE
3122 CGRect r = [parent frame];
3123 r.size = [[UIScreen mainScreen] bounds].size;
3124 [parent setFrame:r];
3125 [self traverseChildren:node on:parent];
3127 # endif // USE_IPHONE
3131 - (void)parser:(NSXMLParser *)parser
3132 didStartElement:(NSString *)elt
3133 namespaceURI:(NSString *)ns
3134 qualifiedName:(NSString *)qn
3135 attributes:(NSDictionary *)attrs
3137 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3139 [e setKind:SimpleXMLElementKind];
3140 [e setAttributesAsDictionary:attrs];
3141 NSXMLElement *p = xml_parsing;
3145 xml_root = xml_parsing;
3148 - (void)parser:(NSXMLParser *)parser
3149 didEndElement:(NSString *)elt
3150 namespaceURI:(NSString *)ns
3151 qualifiedName:(NSString *)qn
3153 NSXMLElement *p = xml_parsing;
3155 NSLog(@"extra close: %@", elt);
3156 } else if (![[p name] isEqualToString:elt]) {
3157 NSLog(@"%@ closed by %@", [p name], elt);
3159 NSXMLElement *n = xml_parsing;
3160 xml_parsing = [n parent];
3165 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3167 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3168 [e setKind:SimpleXMLTextKind];
3169 NSXMLElement *p = xml_parsing;
3171 [e setObjectValue: string];
3177 # ifdef USE_PICKER_VIEW
3179 #pragma mark UIPickerView delegate methods
3181 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3183 return 1; // Columns
3186 - (NSInteger)pickerView:(UIPickerView *)pv
3187 numberOfRowsInComponent:(NSInteger)column
3189 NSAssert (column == 0, @"weird column");
3190 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3191 if (! a) return 0; // Too early?
3195 - (CGFloat)pickerView:(UIPickerView *)pv
3196 rowHeightForComponent:(NSInteger)column
3201 - (CGFloat)pickerView:(UIPickerView *)pv
3202 widthForComponent:(NSInteger)column
3204 NSAssert (column == 0, @"weird column");
3205 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3206 if (! a) return 0; // Too early?
3208 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3210 for (NSArray *a2 in a) {
3211 NSString *s = [a2 objectAtIndex:0];
3212 CGSize r = [s sizeWithFont:f];
3213 if (r.width > max) max = r.width;
3216 max *= 1.7; // WTF!!
3228 - (NSString *)pickerView:(UIPickerView *)pv
3229 titleForRow:(NSInteger)row
3230 forComponent:(NSInteger)column
3232 NSAssert (column == 0, @"weird column");
3233 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3234 if (! a) return 0; // Too early?
3235 a = [a objectAtIndex:row];
3236 NSAssert (a, @"internal error");
3237 return [a objectAtIndex:0];
3240 # endif // USE_PICKER_VIEW
3243 #pragma mark UITableView delegate methods
3245 - (void) addResetButton
3247 [[self navigationItem]
3248 setRightBarButtonItem: [[UIBarButtonItem alloc]
3249 initWithTitle: @"Reset to Defaults"
3250 style: UIBarButtonItemStyleBordered
3252 action:@selector(resetAction:)]];
3253 NSString *s = saver_name;
3254 if ([self view].frame.size.width > 320)
3255 s = [s stringByAppendingString: @" Settings"];
3256 [self navigationItem].title = s;
3260 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3265 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3266 // Number of vertically-stacked white boxes.
3267 return [controls count];
3270 - (NSInteger)tableView:(UITableView *)tableView
3271 numberOfRowsInSection:(NSInteger)section
3273 // Number of lines in each vertically-stacked white box.
3274 NSAssert (controls, @"internal error");
3275 return [[controls objectAtIndex:section] count];
3278 - (NSString *)tableView:(UITableView *)tv
3279 titleForHeaderInSection:(NSInteger)section
3281 // Titles above each vertically-stacked white box.
3282 // if (section == 0)
3283 // return [saver_name stringByAppendingString:@" Settings"];
3288 - (CGFloat)tableView:(UITableView *)tv
3289 heightForRowAtIndexPath:(NSIndexPath *)ip
3293 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3294 objectAtIndex:[ip indexAtPosition:1]];
3296 if ([ctl isKindOfClass:[NSArray class]]) {
3297 NSArray *set = (NSArray *) ctl;
3298 switch ([set count]) {
3299 case 4: // label + left/slider/right.
3300 case 3: // left/slider/right.
3301 h = FONT_SIZE * 3.0;
3303 case 2: // Checkboxes, or text fields.
3304 h = FONT_SIZE * 2.4;
3307 } else if ([ctl isKindOfClass:[UILabel class]]) {
3308 // Radio buttons in a multi-select list.
3309 h = FONT_SIZE * 1.9;
3311 # ifdef USE_HTML_LABELS
3312 } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3314 HTMLLabel *t = (HTMLLabel *) ctl;
3316 r.size.width = [tv frame].size.width;
3317 r.size.width -= LEFT_MARGIN * 2;
3322 # endif // USE_HTML_LABELS
3324 } else { // Does this ever happen?
3325 h = FONT_SIZE + LINE_SPACING * 2;
3328 if (h <= 0) abort();
3333 - (void)refreshTableView
3335 UITableView *tv = (UITableView *) [self view];
3336 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3337 NSInteger rows = [self numberOfSectionsInTableView:tv];
3338 for (int i = 0; i < rows; i++) {
3339 NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3340 for (int j = 0; j < cols; j++) {
3344 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3349 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3352 // Default opacity looks bad.
3353 // #### Oh great, this only works *sometimes*.
3354 UIView *v = [[self navigationItem] titleView];
3355 [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
3359 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3361 [NSTimer scheduledTimerWithTimeInterval: 0
3363 selector:@selector(refreshTableView)
3369 #ifndef USE_PICKER_VIEW
3371 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3372 button:(RadioButton *)b
3374 NSArray *item = [[b items] objectAtIndex: [b index]];
3375 NSString *pref_key = [item objectAtIndex:1];
3376 NSObject *pref_val = [item objectAtIndex:2];
3378 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3380 // Convert them both to strings and compare those, so that
3381 // we don't get screwed by int 1 versus string "1".
3382 // Will boolean true/1 screw us here too?
3384 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3385 ? (NSString *) pref_val
3386 : [(NSNumber *) pref_val stringValue]);
3387 NSString *current_str = ([current isKindOfClass:[NSString class]]
3388 ? (NSString *) current
3389 : [(NSNumber *) current stringValue]);
3390 BOOL match_p = [current_str isEqualToString:pref_str];
3392 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3395 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3397 [cell setAccessoryType:UITableViewCellAccessoryNone];
3401 - (void)tableView:(UITableView *)tv
3402 didSelectRowAtIndexPath:(NSIndexPath *)ip
3404 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3405 objectAtIndex:[ip indexAtPosition:1]];
3406 if (! [ctl isKindOfClass:[RadioButton class]])
3409 [self radioAction:ctl];
3410 [self refreshTableView];
3414 #endif // !USE_PICKER_VIEW
3418 - (UITableViewCell *)tableView:(UITableView *)tv
3419 cellForRowAtIndexPath:(NSIndexPath *)ip
3421 CGFloat ww = [tv frame].size.width;
3422 CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
3424 float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
3426 // Width of the column of labels on the left.
3427 CGFloat left_width = ww * 0.4;
3428 CGFloat right_edge = ww - LEFT_MARGIN;
3430 if (os_version < 7) // margins were wider on iOS 6.1
3433 CGFloat max = FONT_SIZE * 12;
3434 if (left_width > max) left_width = max;
3436 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3437 objectAtIndex:[ip indexAtPosition:1]];
3439 if ([ctl isKindOfClass:[NSArray class]]) {
3440 // This cell has a set of objects in it.
3441 NSArray *set = (NSArray *) ctl;
3442 switch ([set count]) {
3445 // With 2 elements, the first of the pair must be a label.
3446 UILabel *label = (UILabel *) [set objectAtIndex: 0];
3447 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3448 ctl = [set objectAtIndex: 1];
3450 CGRect r = [ctl frame];
3452 if ([ctl isKindOfClass:[UISwitch class]]) { // Checkboxes.
3453 r.size.width = 80; // Magic.
3454 r.origin.x = right_edge - r.size.width + 30; // beats me
3456 if (os_version < 7) // checkboxes were wider on iOS 6.1
3460 r.origin.x = left_width; // Text fields, etc.
3461 r.size.width = right_edge - r.origin.x;
3464 r.origin.y = (hh - r.size.height) / 2; // Center vertically.
3467 // Make a box and put the label and checkbox/slider into it.
3472 NSView *box = [[UIView alloc] initWithFrame:r];
3473 [box addSubview: ctl];
3475 // Let the label make use of any space not taken up by the control.
3477 r.origin.x = LEFT_MARGIN;
3479 r.size.width = [ctl frame].origin.x - r.origin.x;
3482 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3483 [box addSubview: label];
3492 // With 3 elements, 1 and 3 are labels.
3493 // With 4 elements, 1, 2 and 4 are labels.
3495 UILabel *top = ([set count] == 4
3496 ? [set objectAtIndex: i++]
3498 UILabel *left = [set objectAtIndex: i++];
3499 NSView *mid = [set objectAtIndex: i++];
3500 UILabel *right = [set objectAtIndex: i++];
3501 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3502 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3503 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3504 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3506 // 3 elements: control at top of cell.
3507 // 4 elements: center the control vertically.
3508 CGRect r = [mid frame];
3509 r.size.height = 32; // Unchangable height of the slider thumb.
3511 // Center the slider between left_width and right_edge.
3512 # ifdef LABEL_ABOVE_SLIDER
3513 r.origin.x = LEFT_MARGIN;
3515 r.origin.x = left_width;
3517 r.origin.y = (hh - r.size.height) / 2;
3518 r.size.width = right_edge - r.origin.x;
3522 r.size = [[top text] sizeWithFont:[top font]
3524 CGSizeMake (ww - LEFT_MARGIN*2, 100000)
3525 lineBreakMode:[top lineBreakMode]];
3526 # ifdef LABEL_ABOVE_SLIDER
3527 // Top label goes above, flush center/top.
3528 r.origin.x = (ww - r.size.width) / 2;
3530 # else // !LABEL_ABOVE_SLIDER
3531 // Label goes on the left.
3532 r.origin.x = LEFT_MARGIN;
3534 r.size.width = left_width - LEFT_MARGIN;
3536 # endif // !LABEL_ABOVE_SLIDER
3540 // Left label goes under control, flush left/bottom.
3541 r.size = [[left text] sizeWithFont:[left font]
3543 CGSizeMake(ww - LEFT_MARGIN*2, 100000)
3544 lineBreakMode:[left lineBreakMode]];
3545 r.origin.x = [mid frame].origin.x;
3546 r.origin.y = hh - r.size.height - 4;
3549 // Right label goes under control, flush right/bottom.
3551 r.size = [[right text] sizeWithFont:[right font]
3553 CGSizeMake(ww - LEFT_MARGIN*2, 1000000)
3554 lineBreakMode:[right lineBreakMode]];
3555 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3557 r.origin.y = [left frame].origin.y;
3560 // Make a box and put the labels and slider into it.
3565 NSView *box = [[UIView alloc] initWithFrame:r];
3567 [box addSubview: top];
3568 [box addSubview: left];
3569 [box addSubview: right];
3570 [box addSubview: mid];
3577 NSAssert (0, @"unhandled size");
3579 } else { // A single view, not a pair.
3580 CGRect r = [ctl frame];
3581 r.origin.x = LEFT_MARGIN;
3583 r.size.width = right_edge - r.origin.x;
3588 NSString *id = @"Cell";
3589 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
3591 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3592 reuseIdentifier: id]
3595 for (UIView *subview in [cell.contentView subviews])
3596 [subview removeFromSuperview];
3597 [cell.contentView addSubview: ctl];
3598 CGRect r = [ctl frame];
3602 cell.selectionStyle = UITableViewCellSelectionStyleNone;
3603 [cell setAccessoryType:UITableViewCellAccessoryNone];
3605 # ifndef USE_PICKER_VIEW
3606 if ([ctl isKindOfClass:[RadioButton class]])
3607 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3608 # endif // USE_PICKER_VIEW
3612 # endif // USE_IPHONE
3615 /* When this object is instantiated, it parses the XML file and creates
3616 controls on itself that are hooked up to the appropriate preferences.
3617 The default size of the view is just big enough to hold them all.
3619 - (id)initWithXML: (NSData *) xml_data
3620 options: (const XrmOptionDescRec *) _opts
3621 controller: (NSUserDefaultsController *) _prefs
3622 globalController: (NSUserDefaultsController *) _globalPrefs
3623 defaults: (NSDictionary *) _defs
3626 self = [super init];
3627 # else // !USE_IPHONE
3628 self = [super initWithStyle:UITableViewStyleGrouped];
3629 self.title = [saver_name stringByAppendingString:@" Settings"];
3630 # endif // !USE_IPHONE
3631 if (! self) return 0;
3633 // instance variables
3635 defaultOptions = _defs;
3636 userDefaultsController = [_prefs retain];
3637 globalDefaultsController = [_globalPrefs retain];
3639 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3642 NSAssert1 (0, @"XML Error: %@",
3643 [[NSString alloc] initWithData:xml_data
3644 encoding:NSUTF8StringEncoding]);
3647 [xmlDoc setDelegate:self];
3648 if (! [xmlDoc parse]) {
3649 NSError *err = [xmlDoc parserError];
3650 NSAssert2 (0, @"XML Error: %@: %@",
3651 [[NSString alloc] initWithData:xml_data
3652 encoding:NSUTF8StringEncoding],
3658 TextModeTransformer *t = [[TextModeTransformer alloc] init];
3659 [NSValueTransformer setValueTransformer:t
3660 forName:@"TextModeTransformer"];
3662 # endif // USE_IPHONE
3664 [self traverseTree];
3668 [self addResetButton];
3677 [saver_name release];
3678 [userDefaultsController release];
3679 [globalDefaultsController release];
3682 [pref_keys release];
3683 [pref_ctls release];
3684 # ifdef USE_PICKER_VIEW
3685 [picker_values release];