1 /* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski <jwz@jwz.org>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
12 /* XScreenSaver uses XML files to describe the user interface for configuring
13 the various screen savers. These files live in .../hacks/config/ and
14 say relatively high level things like: "there should be a checkbox
15 labelled "Leave Trails", and when it is checked, add the option '-trails'
16 to the command line when launching the program."
18 This code reads that XML and constructs a Cocoa interface from it.
19 The Cocoa controls are hooked up to NSUserDefaultsController to save
20 those settings into the MacOS preferences system. The Cocoa preferences
21 names are the same as the resource names specified in the screenhack's
22 'options' array (we use that array to map the command line switches
23 specified in the XML to the resource names to use).
26 #import "XScreenSaverConfigSheet.h"
30 #import "InvertedSlider.h"
33 # define NSView UIView
34 # define NSRect CGRect
35 # define NSSize CGSize
36 # define NSTextField UITextField
37 # define NSButton UIButton
38 # define NSFont UIFont
39 # define NSStepper UIStepper
40 # define NSMenuItem UIMenuItem
41 # define NSText UILabel
42 # define minValue minimumValue
43 # define maxValue maximumValue
44 # define setMinValue setMinimumValue
45 # define setMaxValue setMaximumValue
46 # define LABEL UILabel
48 # define LABEL NSTextField
51 #undef LABEL_ABOVE_SLIDER
52 #define USE_HTML_LABELS
55 #pragma mark XML Parser
57 /* I used to use the "NSXMLDocument" XML parser, but that doesn't exist
58 on iOS. The "NSXMLParser" parser exists on both OSX and iOS, so I
59 converted to use that. However, to avoid having to re-write all of
60 the old code, I faked out a halfassed implementation of the
61 "NSXMLNode" class that "NSXMLDocument" used to return.
64 #define NSXMLNode SimpleXMLNode
65 #define NSXMLElement SimpleXMLNode
66 #define NSXMLCommentKind SimpleXMLCommentKind
67 #define NSXMLElementKind SimpleXMLElementKind
68 #define NSXMLAttributeKind SimpleXMLAttributeKind
69 #define NSXMLTextKind SimpleXMLTextKind
71 typedef enum { SimpleXMLCommentKind,
73 SimpleXMLAttributeKind,
77 @interface SimpleXMLNode : NSObject
81 SimpleXMLNode *parent;
82 NSMutableArray *children;
83 NSMutableArray *attributes;
87 @property(nonatomic) SimpleXMLKind kind;
88 @property(nonatomic, retain) NSString *name;
89 @property(nonatomic, retain) SimpleXMLNode *parent;
90 @property(nonatomic, retain) NSMutableArray *children;
91 @property(nonatomic, retain) NSMutableArray *attributes;
92 @property(nonatomic, retain, getter=objectValue, setter=setObjectValue:)
97 @implementation SimpleXMLNode
101 //@synthesize parent;
102 @synthesize children;
103 @synthesize attributes;
109 attributes = [NSMutableArray arrayWithCapacity:10];
114 - (id) initWithName:(NSString *)n
117 [self setKind:NSXMLElementKind];
123 - (void) setAttributesAsDictionary:(NSDictionary *)dict
125 for (NSString *key in dict) {
126 NSObject *val = [dict objectForKey:key];
127 SimpleXMLNode *n = [[SimpleXMLNode alloc] init];
128 [n setKind:SimpleXMLAttributeKind];
130 [n setObjectValue:val];
131 [attributes addObject:n];
135 - (SimpleXMLNode *) parent { return parent; }
137 - (void) setParent:(SimpleXMLNode *)p
139 NSAssert (!parent, @"parent already set");
142 NSMutableArray *kids = [p children];
144 kids = [NSMutableArray arrayWithCapacity:10];
145 [p setChildren:kids];
147 [kids addObject:self];
152 #pragma mark textMode value transformer
154 // A value transformer for mapping "url" to "3" and vice versa in the
155 // "textMode" preference, since NSMatrix uses NSInteger selectedIndex.
158 @interface TextModeTransformer: NSValueTransformer {}
160 @implementation TextModeTransformer
161 + (Class)transformedValueClass { return [NSString class]; }
162 + (BOOL)allowsReverseTransformation { return YES; }
164 - (id)transformedValue:(id)value {
165 if ([value isKindOfClass:[NSString class]]) {
167 if ([value isEqualToString:@"date"]) { i = 0; }
168 else if ([value isEqualToString:@"literal"]) { i = 1; }
169 else if ([value isEqualToString:@"file"]) { i = 2; }
170 else if ([value isEqualToString:@"url"]) { i = 3; }
171 else if ([value isEqualToString:@"program"]) { i = 4; }
173 value = [NSNumber numberWithInt: i];
178 - (id)reverseTransformedValue:(id)value {
179 if ([value isKindOfClass:[NSNumber class]]) {
180 switch ((int) [value doubleValue]) {
181 case 0: value = @"date"; break;
182 case 1: value = @"literal"; break;
183 case 2: value = @"file"; break;
184 case 3: value = @"url"; break;
185 case 4: value = @"program"; break;
194 #pragma mark Implementing radio buttons
196 /* The UIPickerView is a hideous and uncustomizable piece of shit.
197 I can't believe Apple actually released that thing on the world.
198 Let's fake up some radio buttons instead.
201 #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
203 @interface RadioButton : UILabel
209 @property(nonatomic) int index;
210 @property(nonatomic, retain) NSArray *items;
214 @implementation RadioButton
219 - (id) initWithIndex:(int)_index items:_items
221 self = [super initWithFrame:CGRectZero];
223 items = [_items retain];
225 [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
226 [self setBackgroundColor:[UIColor clearColor]];
235 # endif // !USE_PICKER_VIEW
238 # pragma mark Implementing labels with clickable links
240 #if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
242 @interface HTMLLabel : UIView <UIWebViewDelegate>
249 @property(nonatomic, retain) NSString *html;
250 @property(nonatomic, retain) UIWebView *webView;
252 - (id) initWithHTML:(NSString *)h font:(UIFont *)f;
253 - (id) initWithText:(NSString *)t font:(UIFont *)f;
254 - (void) setHTML:(NSString *)h;
255 - (void) setText:(NSString *)t;
260 @implementation HTMLLabel
265 - (id) initWithHTML:(NSString *)h font:(UIFont *)f
268 if (! self) return 0;
270 webView = [[UIWebView alloc] init];
271 webView.delegate = self;
272 webView.dataDetectorTypes = UIDataDetectorTypeNone;
273 self. autoresizingMask = UIViewAutoresizingNone; // we do it manually
274 webView.autoresizingMask = UIViewAutoresizingNone;
275 webView.scrollView.scrollEnabled = NO;
276 webView.scrollView.bounces = NO;
278 [webView setBackgroundColor:[UIColor clearColor]];
280 [self addSubview: webView];
285 - (id) initWithText:(NSString *)t font:(UIFont *)f
287 self = [self initWithHTML:@"" font:f];
288 if (! self) return 0;
294 - (void) setHTML: (NSString *)h
298 if (html) [html release];
301 [NSString stringWithFormat:
302 @"<!DOCTYPE HTML PUBLIC "
303 "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
304 " \"http://www.w3.org/TR/html4/loose.dtd\">"
307 // "<META NAME=\"viewport\" CONTENT=\""
308 // "width=device-width"
309 // "initial-scale=1.0;"
310 // "maximum-scale=1.0;\">"
314 " margin: 0; padding: 0; border: 0;"
315 " font-family: \"%@\";"
316 " font-size: %.4fpx;" // Must be "px", not "pt"!
317 " line-height: %.4fpx;" // And no spaces before it.
318 " -webkit-text-size-adjust: none;"
331 [webView stopLoading];
332 [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
336 static char *anchorize (const char *url);
338 - (void) setText: (NSString *)t
340 t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
341 whitespaceCharacterSet]];
342 t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
343 t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
344 t = [t stringByReplacingOccurrencesOfString:@">" withString:@">"];
345 t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
346 t = [t stringByReplacingOccurrencesOfString:@"<P> "
347 withString:@"<P> "];
348 t = [t stringByReplacingOccurrencesOfString:@"\n "
349 withString:@"<BR> "];
353 [t componentsSeparatedByCharactersInSet:
354 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
355 if ([s hasPrefix:@"http://"] ||
356 [s hasPrefix:@"https://"]) {
357 char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
358 NSString *a2 = [NSString stringWithCString: anchor
359 encoding: NSUTF8StringEncoding];
360 s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
363 h = [NSString stringWithFormat: @"%@ %@", h, s];
366 h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"];
367 h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"];
368 h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
369 whitespaceAndNewlineCharacterSet]];
375 -(BOOL) webView:(UIWebView *)wv
376 shouldStartLoadWithRequest:(NSURLRequest *)req
377 navigationType:(UIWebViewNavigationType)type
379 // Force clicked links to open in Safari, not in this window.
380 if (type == UIWebViewNavigationTypeLinkClicked) {
381 [[UIApplication sharedApplication] openURL:[req URL]];
388 - (void) setFrame: (CGRect)r
393 [webView setFrame: r];
397 - (NSString *) stripTags:(NSString *)str
399 NSString *result = @"";
402 str = [str stringByReplacingOccurrencesOfString:@"<P>"
403 withString:@"<BR><BR>"
404 options:NSCaseInsensitiveSearch
405 range:NSMakeRange(0, [str length])];
406 str = [str stringByReplacingOccurrencesOfString:@"<BR>"
408 options:NSCaseInsensitiveSearch
409 range:NSMakeRange(0, [str length])];
412 for (NSString *s in [str componentsSeparatedByString: @"<"]) {
413 NSRange r = [s rangeOfString:@">"];
415 s = [s substringFromIndex: r.location + r.length];
416 result = [result stringByAppendingString: s];
419 // Compress internal horizontal whitespace.
422 for (NSString *s in [str componentsSeparatedByCharactersInSet:
423 [NSCharacterSet whitespaceCharacterSet]]) {
424 if ([result length] == 0)
426 else if ([s length] > 0)
427 result = [NSString stringWithFormat: @"%@ %@", result, s];
436 CGRect r = [self frame];
438 /* It would be sensible to just ask the UIWebView how tall the page is,
439 instead of hoping that NSString and UIWebView measure fonts and do
440 wrapping in exactly the same way, but since UIWebView is asynchronous,
441 we'd have to wait for the document to load first, e.g.:
443 - Start the document loading;
444 - return a default height to use for the UITableViewCell;
445 - wait for the webViewDidFinishLoad delegate method to fire;
446 - then force the UITableView to reload, to pick up the new height.
448 But I couldn't make that work.
451 r.size.height = [[webView
452 stringByEvaluatingJavaScriptFromString:
453 @"document.body.offsetHeight"]
456 NSString *text = [self stripTags: html];
459 s = [text sizeWithFont: font
461 lineBreakMode:NSLineBreakByWordWrapping];
462 r.size.height = s.height;
479 #endif // USE_IPHONE && USE_HTML_LABELS
482 @interface XScreenSaverConfigSheet (Private)
484 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
487 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
488 - (void) placeChild: (NSView *)c on:(NSView *)p;
489 static NSView *last_child (NSView *parent);
490 static void layout_group (NSView *group, BOOL horiz_p);
492 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
493 - (void) placeChild: (NSObject *)c on:(NSView *)p;
494 - (void) placeSeparator;
495 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
496 - (void) refreshTableView;
497 # endif // USE_IPHONE
502 @implementation XScreenSaverConfigSheet
504 # define LEFT_MARGIN 20 // left edge of window
505 # define COLUMN_SPACING 10 // gap between e.g. labels and text fields
506 # define LEFT_LABEL_WIDTH 70 // width of all left labels
507 # define LINE_SPACING 10 // leading between each line
509 # define FONT_SIZE 17 // Magic hardcoded UITableView font size.
511 #pragma mark Talking to the resource database
514 /* Normally we read resources by looking up "KEY" in the database
515 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
516 app, everything is stored in the database "org.jwz.xscreensaver"
517 instead, so transform keys to "SAVERNAME.KEY".
519 NOTE: This is duplicated in PrefsReader.m, cause I suck.
521 - (NSString *) makeKey:(NSString *)key
524 NSString *prefix = [saver_name stringByAppendingString:@"."];
525 if (! [key hasPrefix:prefix]) // Don't double up!
526 key = [prefix stringByAppendingString:key];
532 - (NSString *) makeCKey:(const char *)key
534 return [self makeKey:[NSString stringWithCString:key
535 encoding:NSUTF8StringEncoding]];
539 /* Given a command-line option, returns the corresponding resource name.
540 Any arguments in the switch string are ignored (e.g., "-foo x").
542 - (NSString *) switchToResource:(NSString *)cmdline_switch
543 opts:(const XrmOptionDescRec *)opts_array
544 valRet:(NSString **)val_ret
548 NSAssert(cmdline_switch, @"cmdline switch is null");
549 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
550 encoding:NSUTF8StringEncoding]) {
551 NSAssert1(0, @"unable to convert %@", cmdline_switch);
554 char *s = strpbrk(buf, " \t\r\n");
558 while (*tail && (*tail == ' ' || *tail == '\t'))
562 while (opts_array[0].option) {
563 if (!strcmp (opts_array[0].option, buf)) {
566 if (opts_array[0].argKind == XrmoptionNoArg) {
568 NSAssert1 (0, @"expected no args to switch: \"%@\"",
570 ret = opts_array[0].value;
573 NSAssert1 (0, @"expected args to switch: \"%@\"",
580 ? [NSString stringWithCString:ret
581 encoding:NSUTF8StringEncoding]
584 const char *res = opts_array[0].specifier;
585 while (*res && (*res == '.' || *res == '*'))
587 return [self makeCKey:res];
592 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
597 - (NSUserDefaultsController *)controllerForKey:(NSString *)key
599 static NSDictionary *a = 0;
601 a = UPDATER_DEFAULTS;
604 if ([a objectForKey:key])
605 // These preferences are global to all xscreensavers.
606 return globalDefaultsController;
608 // All other preferences are per-saver.
609 return userDefaultsController;
615 // Called when a slider is bonked.
617 - (void)sliderAction:(UISlider*)sender
619 if ([active_text_field canResignFirstResponder])
620 [active_text_field resignFirstResponder];
621 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
623 // Hacky API. See comment in InvertedSlider.m.
624 double v = ([sender isKindOfClass: [InvertedSlider class]]
625 ? [(InvertedSlider *) sender transformedValue]
628 [[self controllerForKey:pref_key]
629 setObject:((v == (int) v)
630 ? [NSNumber numberWithInt:(int) v]
631 : [NSNumber numberWithDouble: v])
635 // Called when a checkbox/switch is bonked.
637 - (void)switchAction:(UISwitch*)sender
639 if ([active_text_field canResignFirstResponder])
640 [active_text_field resignFirstResponder];
641 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
642 NSString *v = ([sender isOn] ? @"true" : @"false");
643 [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
646 # ifdef USE_PICKER_VIEW
647 // Called when a picker is bonked.
649 - (void)pickerView:(UIPickerView *)pv
650 didSelectRow:(NSInteger)row
651 inComponent:(NSInteger)column
653 if ([active_text_field canResignFirstResponder])
654 [active_text_field resignFirstResponder];
656 NSAssert (column == 0, @"internal error");
657 NSArray *a = [picker_values objectAtIndex: [pv tag]];
658 if (! a) return; // Too early?
659 a = [a objectAtIndex:row];
660 NSAssert (a, @"missing row");
662 //NSString *label = [a objectAtIndex:0];
663 NSString *pref_key = [a objectAtIndex:1];
664 NSObject *pref_val = [a objectAtIndex:2];
665 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
667 # else // !USE_PICKER_VIEW
669 // Called when a RadioButton is bonked.
671 - (void)radioAction:(RadioButton*)sender
673 if ([active_text_field canResignFirstResponder])
674 [active_text_field resignFirstResponder];
676 NSArray *item = [[sender items] objectAtIndex: [sender index]];
677 NSString *pref_key = [item objectAtIndex:1];
678 NSObject *pref_val = [item objectAtIndex:2];
679 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
682 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
684 active_text_field = tf;
688 - (void)textFieldDidEndEditing:(UITextField *)tf
690 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
691 NSString *txt = [tf text];
692 [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
695 - (BOOL)textFieldShouldReturn:(UITextField *)tf
697 active_text_field = nil;
698 [tf resignFirstResponder];
702 # endif // !USE_PICKER_VIEW
709 - (void) okAction:(NSObject *)arg
711 // Without the setAppliesImmediately:, when the saver restarts, it's still
712 // got the old settings. -[XScreenSaverConfigSheet traverseTree] sets this
713 // to NO; default is YES.
714 [userDefaultsController setAppliesImmediately:YES];
715 [globalDefaultsController setAppliesImmediately:YES];
716 [userDefaultsController commitEditing];
717 [globalDefaultsController commitEditing];
718 [userDefaultsController save:self];
719 [globalDefaultsController save:self];
720 [NSApp endSheet:self returnCode:NSOKButton];
724 - (void) cancelAction:(NSObject *)arg
726 [userDefaultsController revert:self];
727 [globalDefaultsController revert:self];
728 [NSApp endSheet:self returnCode:NSCancelButton];
731 # endif // !USE_IPHONE
734 - (void) resetAction:(NSObject *)arg
737 [userDefaultsController revertToInitialValues:self];
738 [globalDefaultsController revertToInitialValues:self];
741 for (NSString *key in defaultOptions) {
742 NSObject *val = [defaultOptions objectForKey:key];
743 [[self controllerForKey:key] setObject:val forKey:key];
746 for (UIControl *ctl in pref_ctls) {
747 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
748 [self bindResource:ctl key:pref_key reload:YES];
751 [self refreshTableView];
752 # endif // USE_IPHONE
756 /* Connects a control (checkbox, etc) to the corresponding preferences key.
758 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
759 reload:(BOOL)reload_p
761 NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
763 NSDictionary *opts_dict = nil;
764 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
766 : ([control isKindOfClass:[NSMatrix class]]
770 if ([control isKindOfClass:[NSMatrix class]]) {
771 opts_dict = @{ NSValueTransformerNameBindingOption:
772 @"TextModeTransformer" };
777 withKeyPath:[@"values." stringByAppendingString: pref_key]
782 NSObject *val = [prefs objectForKey:pref_key];
786 if ([val isKindOfClass:[NSString class]]) {
787 sval = (NSString *) val;
788 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
789 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
790 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
793 dval = [sval doubleValue];
794 } else if ([val isKindOfClass:[NSNumber class]]) {
795 // NSBoolean (__NSCFBoolean) is really NSNumber.
796 dval = [(NSNumber *) val doubleValue];
797 sval = [(NSNumber *) val stringValue];
800 if ([control isKindOfClass:[UISlider class]]) {
801 sel = @selector(sliderAction:);
802 // Hacky API. See comment in InvertedSlider.m.
803 if ([control isKindOfClass:[InvertedSlider class]])
804 [(InvertedSlider *) control setTransformedValue: dval];
806 [(UISlider *) control setValue: dval];
807 } else if ([control isKindOfClass:[UISwitch class]]) {
808 sel = @selector(switchAction:);
809 [(UISwitch *) control setOn: ((int) dval != 0)];
810 # ifdef USE_PICKER_VIEW
811 } else if ([control isKindOfClass:[UIPickerView class]]) {
813 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
815 # else // !USE_PICKER_VIEW
816 } else if ([control isKindOfClass:[RadioButton class]]) {
817 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
818 } else if ([control isKindOfClass:[UITextField class]]) {
820 [(UITextField *) control setText: sval];
821 # endif // !USE_PICKER_VIEW
823 NSAssert (0, @"unknown class");
826 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
830 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
831 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
834 [pref_keys addObject: [self makeKey:pref_key]];
835 [pref_ctls addObject: control];
836 ((UIControl *) control).tag = [pref_keys count] - 1;
839 [(UIControl *) control addTarget:self action:sel
840 forControlEvents:UIControlEventValueChanged];
844 # endif // USE_IPHONE
847 NSObject *def = [[prefs defaults] objectForKey:pref_key];
848 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
849 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
850 s = [NSString stringWithFormat:@"%@ = %@", s,
851 ([def isKindOfClass:[NSString class]]
852 ? [NSString stringWithFormat:@"\"%@\"", def]
854 s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0];
855 s = [NSString stringWithFormat:@"%@ %@ / %@", s,
856 [def class], [control class]];
858 s = [NSString stringWithFormat:@"%@ / %@", s, bindto];
865 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
867 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
872 - (void) bindSwitch:(NSObject *)control
873 cmdline:(NSString *)cmd
875 [self bindResource:control
876 key:[self switchToResource:cmd opts:opts valRet:0]];
880 #pragma mark Text-manipulating utilities
884 unwrap (NSString *text)
886 // Unwrap lines: delete \n but do not delete \n\n.
888 NSArray *lines = [text componentsSeparatedByString:@"\n"];
889 NSUInteger i, nlines = [lines count];
892 text = @"\n"; // start with one blank line
894 // skip trailing blank lines in file
895 for (i = nlines-1; i > 0; i--) {
896 NSString *s = (NSString *) [lines objectAtIndex:i];
902 // skip leading blank lines in file
903 for (i = 0; i < nlines; i++) {
904 NSString *s = (NSString *) [lines objectAtIndex:i];
911 for (; i < nlines; i++) {
912 NSString *s = (NSString *) [lines objectAtIndex:i];
913 if ([s length] == 0) {
914 text = [text stringByAppendingString:@"\n\n"];
916 } else if ([s characterAtIndex:0] == ' ' ||
917 [s hasPrefix:@"Copyright "] ||
918 [s hasPrefix:@"http://"]) {
919 // don't unwrap if the following line begins with whitespace,
920 // or with the word "Copyright", or if it begins with a URL.
922 text = [text stringByAppendingString:@"\n"];
923 text = [text stringByAppendingString:s];
928 text = [text stringByAppendingString:@" "];
929 text = [text stringByAppendingString:s];
940 /* Makes the text up to the first comma be bold.
943 boldify (NSText *nstext)
945 NSString *text = [nstext string];
946 NSRange r = [text rangeOfString:@"," options:0];
947 r.length = r.location+1;
951 NSFont *font = [nstext font];
952 font = [NSFont boldSystemFontOfSize:[font pointSize]];
953 [nstext setFont:font range:r];
955 # endif // !USE_IPHONE
958 /* Creates a human-readable anchor to put on a URL.
961 anchorize (const char *url)
963 const char *wiki = "http://en.wikipedia.org/wiki/";
964 const char *math = "http://mathworld.wolfram.com/";
965 if (!strncmp (wiki, url, strlen(wiki))) {
966 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
967 strcpy (anchor, "Wikipedia: \"");
968 const char *in = url + strlen(wiki);
969 char *out = anchor + strlen(anchor);
973 } else if (*in == '#') {
976 } else if (*in == '%') {
982 sscanf (hex, "%x", &n);
994 } else if (!strncmp (math, url, strlen(math))) {
995 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
996 strcpy (anchor, "MathWorld: \"");
997 const char *start = url + strlen(wiki);
998 const char *in = start;
999 char *out = anchor + strlen(anchor);
1003 } else if (in != start && *in >= 'A' && *in <= 'Z') {
1006 } else if (!strncmp (in, ".htm", 4)) {
1018 return strdup (url);
1023 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
1025 /* Converts any http: URLs in the given text field to clickable links.
1028 hreffify (NSText *nstext)
1031 NSString *text = [nstext string];
1032 [nstext setRichText:YES];
1034 NSString *text = [nstext text];
1037 int L = [text length];
1038 NSRange start; // range is start-of-search to end-of-string
1041 while (start.location < L) {
1043 // Find the beginning of a URL...
1045 NSRange r2 = [text rangeOfString:@"http://" options:0 range:start];
1046 if (r2.location == NSNotFound)
1049 // Next time around, start searching after this.
1050 start.location = r2.location + r2.length;
1051 start.length = L - start.location;
1053 // Find the end of a URL (whitespace or EOF)...
1055 NSRange r3 = [text rangeOfCharacterFromSet:
1056 [NSCharacterSet whitespaceAndNewlineCharacterSet]
1057 options:0 range:start];
1058 if (r3.location == NSNotFound) // EOF
1059 r3.location = L, r3.length = 0;
1061 // Next time around, start searching after this.
1062 start.location = r3.location;
1063 start.length = L - start.location;
1065 // Set r2 to the start/length of this URL.
1066 r2.length = start.location - r2.location;
1069 NSString *nsurl = [text substringWithRange:r2];
1070 const char *url = [nsurl UTF8String];
1072 // If this is a Wikipedia URL, make the linked text be prettier.
1074 char *anchor = anchorize(url);
1078 // Construct the RTF corresponding to <A HREF="url">anchor</A>
1080 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1081 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1082 sprintf (rtf, fmt, url, anchor);
1084 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1085 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1087 # else // !USE_IPHONE
1088 // *anchor = 0; // Omit Wikipedia anchor
1089 text = [text stringByReplacingCharactersInRange:r2
1090 withString:[NSString stringWithCString:anchor
1091 encoding:NSUTF8StringEncoding]];
1092 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1093 // withString:@"\n\n"];
1094 # endif // !USE_IPHONE
1098 int L2 = [text length]; // might have changed
1099 start.location -= (L - L2);
1104 [nstext setText:text];
1109 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1113 #pragma mark Creating controls from XML
1116 /* Parse the attributes of an XML tag into a dictionary.
1117 For input, the dictionary should have as attributes the keys, each
1118 with @"" as their value.
1119 On output, the dictionary will set the keys to the values specified,
1120 and keys that were not specified will not be present in the dictionary.
1121 Warnings are printed if there are duplicate or unknown attributes.
1123 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1125 NSArray *attrs = [(NSXMLElement *) node attributes];
1126 NSUInteger n = [attrs count];
1129 // For each key in the dictionary, fill in the dict with the corresponding
1130 // value. The value @"" is assumed to mean "un-set". Issue a warning if
1131 // an attribute is specified twice.
1133 for (i = 0; i < n; i++) {
1134 NSXMLNode *attr = [attrs objectAtIndex:i];
1135 NSString *key = [attr name];
1136 NSString *val = [attr objectValue];
1137 NSString *old = [dict objectForKey:key];
1140 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1141 } else if ([old length] != 0) {
1142 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1144 [dict setValue:val forKey:key];
1148 // Remove from the dictionary any keys whose value is still @"",
1149 // meaning there was no such attribute specified.
1151 NSArray *keys = [dict allKeys];
1153 for (i = 0; i < n; i++) {
1154 NSString *key = [keys objectAtIndex:i];
1155 NSString *val = [dict objectForKey:key];
1156 if ([val length] == 0)
1157 [dict removeObjectForKey:key];
1161 // Kludge for starwars.xml:
1162 // If there is a "_low-label" and no "_label", but "_low-label" contains
1163 // spaces, divide them.
1164 NSString *lab = [dict objectForKey:@"_label"];
1165 NSString *low = [dict objectForKey:@"_low-label"];
1168 [[[low stringByTrimmingCharactersInSet:
1169 [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1170 componentsSeparatedByString: @" "]
1171 filteredArrayUsingPredicate:
1172 [NSPredicate predicateWithFormat:@"length > 0"]];
1173 if (split && [split count] == 2) {
1174 [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1175 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1178 # endif // USE_IPHONE
1182 /* Handle the options on the top level <xscreensaver> tag.
1184 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1186 NSMutableDictionary *dict = [@{ @"name": @"",
1190 [self parseAttrs:dict node:node];
1191 NSString *name = [dict objectForKey:@"name"];
1192 NSString *label = [dict objectForKey:@"_label"];
1194 NSAssert1 (label, @"no _label in %@", [node name]);
1195 NSAssert1 (name, @"no name in \"%@\"", label);
1200 /* Creates a label: an un-editable NSTextField displaying the given text.
1202 - (LABEL *) makeLabel:(NSString *)text
1205 rect.origin.x = rect.origin.y = 0;
1206 rect.size.width = rect.size.height = 10;
1208 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1209 [lab setSelectable:NO];
1210 [lab setEditable:NO];
1211 [lab setBezeled:NO];
1212 [lab setDrawsBackground:NO];
1213 [lab setStringValue:text];
1215 # else // USE_IPHONE
1216 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1217 [lab setText: [text stringByTrimmingCharactersInSet:
1218 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1219 [lab setBackgroundColor:[UIColor clearColor]];
1220 [lab setNumberOfLines:0]; // unlimited
1221 // [lab setLineBreakMode:UILineBreakModeWordWrap];
1222 [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1223 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1224 UIViewAutoresizingFlexibleHeight)];
1225 # endif // USE_IPHONE
1230 /* Creates the checkbox (NSButton) described by the given XML node.
1232 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1234 NSMutableDictionary *dict = [@{ @"id": @"",
1239 [self parseAttrs:dict node:node];
1240 NSString *label = [dict objectForKey:@"_label"];
1241 NSString *arg_set = [dict objectForKey:@"arg-set"];
1242 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1245 NSAssert1 (0, @"no _label in %@", [node name]);
1248 if (!arg_set && !arg_unset) {
1249 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
1252 if (arg_set && arg_unset) {
1253 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
1257 // sanity-check the choice of argument names.
1259 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1260 [arg_set hasPrefix:@"--no-"]))
1261 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1263 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1264 ![arg_unset hasPrefix:@"--no-"]))
1265 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1269 rect.origin.x = rect.origin.y = 0;
1270 rect.size.width = rect.size.height = 10;
1274 NSButton *button = [[NSButton alloc] initWithFrame:rect];
1275 [button setButtonType:NSSwitchButton];
1276 [button setTitle:label];
1278 [self placeChild:button on:parent];
1280 # else // USE_IPHONE
1282 LABEL *lab = [self makeLabel:label];
1283 [self placeChild:lab on:parent];
1284 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1285 [self placeChild:button on:parent right:YES];
1288 # endif // USE_IPHONE
1290 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1295 /* Creates the number selection control described by the given XML node.
1296 If "type=slider", it's an NSSlider.
1297 If "type=spinbutton", it's a text field with up/down arrows next to it.
1299 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1301 NSMutableDictionary *dict = [@{ @"id": @"",
1304 @"_high-label": @"",
1312 [self parseAttrs:dict node:node];
1313 NSString *label = [dict objectForKey:@"_label"];
1314 NSString *low_label = [dict objectForKey:@"_low-label"];
1315 NSString *high_label = [dict objectForKey:@"_high-label"];
1316 NSString *type = [dict objectForKey:@"type"];
1317 NSString *arg = [dict objectForKey:@"arg"];
1318 NSString *low = [dict objectForKey:@"low"];
1319 NSString *high = [dict objectForKey:@"high"];
1320 NSString *def = [dict objectForKey:@"default"];
1321 NSString *cvt = [dict objectForKey:@"convert"];
1323 NSAssert1 (arg, @"no arg in %@", label);
1324 NSAssert1 (type, @"no type in %@", label);
1327 NSAssert1 (0, @"no low in %@", [node name]);
1331 NSAssert1 (0, @"no high in %@", [node name]);
1335 NSAssert1 (0, @"no default in %@", [node name]);
1338 if (cvt && ![cvt isEqualToString:@"invert"]) {
1339 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1343 // If either the min or max field contains a decimal point, then this
1344 // option may have a floating point value; otherwise, it is constrained
1345 // to be an integer.
1347 NSCharacterSet *dot =
1348 [NSCharacterSet characterSetWithCharactersInString:@"."];
1349 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1350 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1352 if ([type isEqualToString:@"slider"]
1353 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1354 || [type isEqualToString:@"spinbutton"]
1359 rect.origin.x = rect.origin.y = 0;
1360 rect.size.width = 150;
1361 rect.size.height = 23; // apparent min height for slider with ticks...
1363 slider = [[InvertedSlider alloc] initWithFrame:rect
1365 integers: !float_p];
1366 [slider setMaxValue:[high doubleValue]];
1367 [slider setMinValue:[low doubleValue]];
1369 int range = [slider maxValue] - [slider minValue] + 1;
1372 while (range2 > max_ticks)
1375 // If we have elided ticks, leave it at the max number of ticks.
1376 if (range != range2 && range2 < max_ticks)
1379 // If it's a float, always display the max number of ticks.
1380 if (float_p && range2 < max_ticks)
1384 [slider setNumberOfTickMarks:range2];
1386 [slider setAllowsTickMarkValuesOnly:
1387 (range == range2 && // we are showing the actual number of ticks
1388 !float_p)]; // and we want integer results
1389 # endif // !USE_IPHONE
1391 // #### Note: when the slider's range is large enough that we aren't
1392 // showing all possible ticks, the slider's value is not constrained
1393 // to be an integer, even though it should be...
1394 // Maybe we need to use a value converter or something?
1398 lab = [self makeLabel:label];
1399 [self placeChild:lab on:parent];
1402 CGFloat s = [NSFont systemFontSize] + 4;
1403 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1410 lab = [self makeLabel:low_label];
1411 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1413 [lab setAlignment:1]; // right aligned
1415 if (rect.size.width < LEFT_LABEL_WIDTH)
1416 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1417 rect.size.height = [slider frame].size.height;
1418 [lab setFrame:rect];
1419 [self placeChild:lab on:parent];
1420 # else // USE_IPHONE
1421 [lab setTextAlignment: NSTextAlignmentRight];
1422 // Sometimes rotation screws up truncation.
1423 [lab setLineBreakMode:NSLineBreakByClipping];
1424 [self placeChild:lab on:parent right:(label ? YES : NO)];
1425 # endif // USE_IPHONE
1431 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1432 # else // USE_IPHONE
1433 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1434 # endif // USE_IPHONE
1437 // Make left label be same height as slider.
1439 rect.size.height = [slider frame].size.height;
1440 [lab setFrame:rect];
1444 rect = [slider frame];
1445 if (rect.origin.x < LEFT_LABEL_WIDTH)
1446 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1447 [slider setFrame:rect];
1451 lab = [self makeLabel:high_label];
1452 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1455 // Make right label be same height as slider.
1456 rect.size.height = [slider frame].size.height;
1457 [lab setFrame:rect];
1459 // Sometimes rotation screws up truncation.
1460 [lab setLineBreakMode:NSLineBreakByClipping];
1462 [self placeChild:lab on:parent right:YES];
1466 [self bindSwitch:slider cmdline:arg];
1469 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1471 } else if ([type isEqualToString:@"spinbutton"]) {
1474 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1477 NSAssert1 (!low_label,
1478 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1479 NSAssert1 (!high_label,
1480 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1481 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1485 rect.origin.x = rect.origin.y = 0;
1486 rect.size.width = rect.size.height = 10;
1488 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1489 [txt setStringValue:@"0000.0"];
1491 [txt setStringValue:@""];
1494 LABEL *lab = [self makeLabel:label];
1495 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1496 [lab setAlignment:1]; // right aligned
1498 if (rect.size.width < LEFT_LABEL_WIDTH)
1499 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1500 rect.size.height = [txt frame].size.height;
1501 [lab setFrame:rect];
1502 [self placeChild:lab on:parent];
1506 [self placeChild:txt on:parent right:(label ? YES : NO)];
1510 if (rect.origin.x < LEFT_LABEL_WIDTH)
1511 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1512 [txt setFrame:rect];
1515 rect.size.width = rect.size.height = 10;
1516 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1518 [self placeChild:step on:parent right:YES];
1519 rect = [step frame];
1520 rect.origin.x -= COLUMN_SPACING; // this one goes close
1521 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1522 [step setFrame:rect];
1524 [step setMinValue:[low doubleValue]];
1525 [step setMaxValue:[high doubleValue]];
1526 [step setAutorepeat:YES];
1527 [step setValueWraps:NO];
1529 double range = [high doubleValue] - [low doubleValue];
1531 [step setIncrement:range / 10.0];
1532 else if (range >= 500)
1533 [step setIncrement:range / 100.0];
1535 [step setIncrement:1.0];
1537 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1538 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1539 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1540 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1541 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1542 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1543 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1545 [fmt setGeneratesDecimalNumbers:float_p];
1546 [[txt cell] setFormatter:fmt];
1548 [self bindSwitch:step cmdline:arg];
1549 [self bindSwitch:txt cmdline:arg];
1554 # endif // USE_IPHONE
1557 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1564 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1566 /* If the object associated with this menu item looks like a boolean,
1567 store an NSNumber instead of an NSString, since that's what
1568 will be in the preferences (due to similar logic in PrefsReader).
1570 if ([obj isKindOfClass:[NSString class]]) {
1571 NSString *string = (NSString *) obj;
1572 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1573 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1574 obj = [NSNumber numberWithBool:YES];
1575 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1576 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1577 obj = [NSNumber numberWithBool:NO];
1582 [item setRepresentedObject:obj];
1583 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1585 # endif // !USE_IPHONE
1588 /* Creates the popup menu described by the given XML node (and its children).
1590 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1592 NSArray *children = [node children];
1593 NSUInteger i, count = [children count];
1596 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1600 // get the "id" attribute off the <select> tag.
1602 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1603 [self parseAttrs:dict node:node];
1606 rect.origin.x = rect.origin.y = 0;
1607 rect.size.width = 10;
1608 rect.size.height = 10;
1610 NSString *menu_key = nil; // the resource key used by items in this menu
1613 // #### "Build and Analyze" says that all of our widgets leak, because it
1614 // seems to not realize that placeChild -> addSubview retains them.
1615 // Not sure what to do to make these warnings go away.
1617 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1619 NSMenuItem *def_item = nil;
1620 float max_width = 0;
1622 # else // USE_IPHONE
1624 NSString *def_item = nil;
1626 rect.size.width = 0;
1627 rect.size.height = 0;
1628 # ifdef USE_PICKER_VIEW
1629 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1630 popup.delegate = self;
1631 popup.dataSource = self;
1632 # endif // !USE_PICKER_VIEW
1633 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1635 # endif // USE_IPHONE
1637 for (i = 0; i < count; i++) {
1638 NSXMLNode *child = [children objectAtIndex:i];
1640 if ([child kind] == NSXMLCommentKind)
1642 if ([child kind] != NSXMLElementKind) {
1643 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1647 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1649 NSMutableDictionary *dict2 = [@{ @"id": @"",
1653 [self parseAttrs:dict2 node:child];
1654 NSString *label = [dict2 objectForKey:@"_label"];
1655 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1658 NSAssert1 (0, @"no _label in %@", [child name]);
1663 // create the menu item (and then get a pointer to it)
1664 [popup addItemWithTitle:label];
1665 NSMenuItem *item = [popup itemWithTitle:label];
1666 # endif // USE_IPHONE
1669 NSString *this_val = NULL;
1670 NSString *this_key = [self switchToResource: arg_set
1673 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1674 if (menu_key && ![menu_key isEqualToString:this_key])
1676 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1677 menu_key, this_key, this_val);
1679 menu_key = this_key;
1681 /* If this menu has the cmd line "-mode foo" then set this item's
1682 value to "foo" (the menu itself will be bound to e.g. "modeString")
1685 set_menu_item_object (item, this_val);
1687 // Array holds ["Label", "resource-key", "resource-val"].
1688 [items addObject:[NSMutableArray arrayWithObjects:
1689 label, @"", this_val, nil]];
1693 // no arg-set -- only one menu item can be missing that.
1694 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1699 // Array holds ["Label", "resource-key", "resource-val"].
1700 [items addObject:[NSMutableArray arrayWithObjects:
1701 label, @"", @"", nil]];
1705 /* make sure the menu button has room for the text of this item,
1706 and remember the greatest width it has reached.
1709 [popup setTitle:label];
1711 NSRect r = [popup frame];
1712 if (r.size.width > max_width) max_width = r.size.width;
1713 # endif // USE_IPHONE
1717 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1721 /* We've added all of the menu items. If there was an item with no
1722 command-line switch, then it's the item that represents the default
1723 value. Now we must bind to that item as well... (We have to bind
1724 this one late, because if it was the first item, then we didn't
1725 yet know what resource was associated with this menu.)
1728 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1730 @"no default value for resource \"%@\" in menu item \"%@\"",
1740 set_menu_item_object (def_item, def_obj);
1741 # else // !USE_IPHONE
1742 for (NSMutableArray *a in items) {
1743 // Make sure each array contains the resource key.
1744 [a replaceObjectAtIndex:1 withObject:menu_key];
1745 // Make sure the default item contains the default resource value.
1746 if (def_obj && def_item &&
1747 [def_item isEqualToString:[a objectAtIndex:0]])
1748 [a replaceObjectAtIndex:2 withObject:def_obj];
1750 # endif // !USE_IPHONE
1754 # ifdef USE_PICKER_VIEW
1755 /* Finish tweaking the menu button itself.
1758 [popup setTitle:[def_item title]];
1759 NSRect r = [popup frame];
1760 r.size.width = max_width;
1762 # endif // USE_PICKER_VIEW
1765 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1766 [self placeChild:popup on:parent];
1767 [self bindResource:popup key:menu_key];
1772 # ifdef USE_PICKER_VIEW
1773 // Store the items for this picker in the picker_values array.
1774 // This is so fucking stupid.
1776 unsigned long menu_number = [pref_keys count] - 1;
1777 if (! picker_values)
1778 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1779 while ([picker_values count] <= menu_number)
1780 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1781 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1782 [popup reloadAllComponents];
1784 # else // !USE_PICKER_VIEW
1786 [self placeSeparator];
1789 for (__attribute__((unused)) NSArray *item in items) {
1790 RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1792 [b setLineBreakMode:NSLineBreakByTruncatingHead];
1793 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1794 [self placeChild:b on:parent];
1798 [self placeSeparator];
1800 # endif // !USE_PICKER_VIEW
1801 # endif // !USE_IPHONE
1806 /* Creates an uneditable, wrapping NSTextField to display the given
1807 text enclosed by <description> ... </description> in the XML.
1809 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1811 NSString *text = nil;
1812 NSArray *children = [node children];
1813 NSUInteger i, count = [children count];
1815 for (i = 0; i < count; i++) {
1816 NSXMLNode *child = [children objectAtIndex:i];
1817 NSString *s = [child objectValue];
1819 text = [text stringByAppendingString:s];
1824 text = unwrap (text);
1826 NSRect rect = [parent frame];
1827 rect.origin.x = rect.origin.y = 0;
1828 rect.size.width = 200;
1829 rect.size.height = 50; // sized later
1831 NSText *lab = [[NSText alloc] initWithFrame:rect];
1832 [lab setEditable:NO];
1833 [lab setDrawsBackground:NO];
1834 [lab setHorizontallyResizable:YES];
1835 [lab setVerticallyResizable:YES];
1836 [lab setString:text];
1841 # else // USE_IPHONE
1843 # ifndef USE_HTML_LABELS
1845 UILabel *lab = [self makeLabel:text];
1846 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1849 # else // USE_HTML_LABELS
1850 HTMLLabel *lab = [[HTMLLabel alloc]
1852 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1853 [lab setFrame:rect];
1855 # endif // USE_HTML_LABELS
1857 [self placeSeparator];
1859 # endif // USE_IPHONE
1861 [self placeChild:lab on:parent];
1866 /* Creates the NSTextField described by the given XML node.
1868 - (void) makeTextField: (NSXMLNode *)node
1869 on: (NSView *)parent
1870 withLabel: (BOOL) label_p
1871 horizontal: (BOOL) horiz_p
1873 NSMutableDictionary *dict = [@{ @"id": @"",
1877 [self parseAttrs:dict node:node];
1878 NSString *label = [dict objectForKey:@"_label"];
1879 NSString *arg = [dict objectForKey:@"arg"];
1881 if (!label && label_p) {
1882 NSAssert1 (0, @"no _label in %@", [node name]);
1886 NSAssert1 (arg, @"no arg in %@", label);
1889 rect.origin.x = rect.origin.y = 0;
1890 rect.size.width = rect.size.height = 10;
1892 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1896 // make the default size be around 30 columns; a typical value for
1897 // these text fields is "xscreensaver-text --cols 40".
1899 [txt setStringValue:@"123456789 123456789 123456789 "];
1901 [[txt cell] setWraps:NO];
1902 [[txt cell] setScrollable:YES];
1903 [txt setStringValue:@""];
1905 # else // USE_IPHONE
1907 txt.adjustsFontSizeToFitWidth = YES;
1908 txt.textColor = [UIColor blackColor];
1909 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1910 txt.placeholder = @"";
1911 txt.borderStyle = UITextBorderStyleRoundedRect;
1912 txt.textAlignment = NSTextAlignmentRight;
1913 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1914 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1915 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1916 txt.clearButtonMode = UITextFieldViewModeAlways;
1917 txt.returnKeyType = UIReturnKeyDone;
1918 txt.delegate = self;
1920 [txt setEnabled: YES];
1922 rect.size.height = [txt.font lineHeight] * 1.2;
1923 [txt setFrame:rect];
1925 # endif // USE_IPHONE
1928 LABEL *lab = [self makeLabel:label];
1929 [self placeChild:lab on:parent];
1933 [self placeChild:txt on:parent right:(label ? YES : NO)];
1935 [self bindSwitch:txt cmdline:arg];
1940 /* Creates the NSTextField described by the given XML node,
1941 and hooks it up to a Choose button and a file selector widget.
1943 - (void) makeFileSelector: (NSXMLNode *)node
1944 on: (NSView *)parent
1945 dirsOnly: (BOOL) dirsOnly
1946 withLabel: (BOOL) label_p
1947 editable: (BOOL) editable_p
1949 # ifndef USE_IPHONE // No files. No selectors.
1950 NSMutableDictionary *dict = [@{ @"id": @"",
1954 [self parseAttrs:dict node:node];
1955 NSString *label = [dict objectForKey:@"_label"];
1956 NSString *arg = [dict objectForKey:@"arg"];
1958 if (!label && label_p) {
1959 NSAssert1 (0, @"no _label in %@", [node name]);
1963 NSAssert1 (arg, @"no arg in %@", label);
1966 rect.origin.x = rect.origin.y = 0;
1967 rect.size.width = rect.size.height = 10;
1969 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1971 // make the default size be around 20 columns.
1973 [txt setStringValue:@"123456789 123456789 "];
1975 [txt setSelectable:YES];
1976 [txt setEditable:editable_p];
1977 [txt setBezeled:editable_p];
1978 [txt setDrawsBackground:editable_p];
1979 [[txt cell] setWraps:NO];
1980 [[txt cell] setScrollable:YES];
1981 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
1982 [txt setStringValue:@""];
1986 lab = [self makeLabel:label];
1987 [self placeChild:lab on:parent];
1991 [self placeChild:txt on:parent right:(label ? YES : NO)];
1993 [self bindSwitch:txt cmdline:arg];
1996 // Make the text field and label be the same height, whichever is taller.
1999 rect.size.height = ([lab frame].size.height > [txt frame].size.height
2000 ? [lab frame].size.height
2001 : [txt frame].size.height);
2002 [txt setFrame:rect];
2005 // Now put a "Choose" button next to it.
2007 rect.origin.x = rect.origin.y = 0;
2008 rect.size.width = rect.size.height = 10;
2009 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
2010 [choose setTitle:@"Choose..."];
2011 [choose setBezelStyle:NSRoundedBezelStyle];
2014 [self placeChild:choose on:parent right:YES];
2016 // center the Choose button around the midpoint of the text field.
2017 rect = [choose frame];
2018 rect.origin.y = ([txt frame].origin.y +
2019 (([txt frame].size.height - rect.size.height) / 2));
2020 [choose setFrameOrigin:rect.origin];
2022 [choose setTarget:[parent window]];
2024 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
2026 [choose setAction:@selector(fileSelectorChooseAction:)];
2029 # endif // !USE_IPHONE
2035 /* Runs a modal file selector and sets the text field's value to the
2036 selected file or directory.
2039 do_file_selector (NSTextField *txt, BOOL dirs_p)
2041 NSOpenPanel *panel = [NSOpenPanel openPanel];
2042 [panel setAllowsMultipleSelection:NO];
2043 [panel setCanChooseFiles:!dirs_p];
2044 [panel setCanChooseDirectories:dirs_p];
2046 NSString *file = [txt stringValue];
2047 if ([file length] <= 0) {
2048 file = NSHomeDirectory();
2050 file = [file stringByAppendingPathComponent:@"Pictures"];
2053 // NSString *dir = [file stringByDeletingLastPathComponent];
2055 int result = [panel runModalForDirectory:file //dir
2056 file:nil //[file lastPathComponent]
2058 if (result == NSOKButton) {
2059 NSArray *files = [panel filenames];
2060 file = ([files count] > 0 ? [files objectAtIndex:0] : @"");
2061 file = [file stringByAbbreviatingWithTildeInPath];
2062 [txt setStringValue:file];
2064 // Fuck me! Just setting the value of the NSTextField does not cause
2065 // that to end up in the preferences!
2067 NSDictionary *dict = [txt infoForBinding:@"value"];
2068 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
2069 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
2070 if ([path hasPrefix:@"values."]) // WTF.
2071 path = [path substringFromIndex:7];
2072 [[prefs values] setValue:file forKey:path];
2075 // make sure the end of the string is visible.
2076 NSText *fe = [[txt window] fieldEditor:YES forObject:txt];
2078 range.location = [file length]-3;
2080 if (! [[txt window] makeFirstResponder:[txt window]])
2081 [[txt window] endEditingFor:nil];
2082 // [[txt window] makeFirstResponder:nil];
2083 [fe setSelectedRange:range];
2084 // [tv scrollRangeToVisible:range];
2085 // [txt setNeedsDisplay:YES];
2086 // [[txt cell] setNeedsDisplay:YES];
2087 // [txt selectAll:txt];
2093 /* Returns the NSTextField that is to the left of or above the NSButton.
2095 static NSTextField *
2096 find_text_field_of_button (NSButton *button)
2098 NSView *parent = [button superview];
2099 NSArray *kids = [parent subviews];
2100 int nkids = [kids count];
2103 for (i = 0; i < nkids; i++) {
2104 NSObject *kid = [kids objectAtIndex:i];
2105 if ([kid isKindOfClass:[NSTextField class]]) {
2106 f = (NSTextField *) kid;
2107 } else if (kid == button) {
2116 - (void) fileSelectorChooseAction:(NSObject *)arg
2118 NSButton *choose = (NSButton *) arg;
2119 NSTextField *txt = find_text_field_of_button (choose);
2120 do_file_selector (txt, NO);
2123 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2125 NSButton *choose = (NSButton *) arg;
2126 NSTextField *txt = find_text_field_of_button (choose);
2127 do_file_selector (txt, YES);
2130 #endif // !USE_IPHONE
2133 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2138 (x) Computer name and time
2139 ( ) Text [__________________________]
2140 ( ) Text file [_________________] [Choose]
2141 ( ) URL [__________________________]
2142 ( ) Shell Cmd [__________________________]
2144 textMode -text-mode date
2145 textMode -text-mode literal textLiteral -text-literal %
2146 textMode -text-mode file textFile -text-file %
2147 textMode -text-mode url textURL -text-url %
2148 textMode -text-mode program textProgram -text-program %
2151 rect.size.width = rect.size.height = 1;
2152 rect.origin.x = rect.origin.y = 0;
2153 NSView *group = [[NSView alloc] initWithFrame:rect];
2154 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2156 Bool program_p = TRUE;
2161 // This is how you link radio buttons together.
2163 NSButtonCell *proto = [[NSButtonCell alloc] init];
2164 [proto setButtonType:NSRadioButton];
2166 rect.origin.x = rect.origin.y = 0;
2167 rect.size.width = rect.size.height = 10;
2168 NSMatrix *matrix = [[NSMatrix alloc]
2170 mode:NSRadioModeMatrix
2172 numberOfRows: 4 + (program_p ? 1 : 0)
2174 [matrix setAllowsEmptySelection:NO];
2176 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
2177 [cnames addObject:@"Computer name and time"];
2178 [cnames addObject:@"Text"];
2179 [cnames addObject:@"File"];
2180 [cnames addObject:@"URL"];
2181 if (program_p) [cnames addObject:@"Shell Cmd"];
2182 [matrix bind:@"content"
2184 withKeyPath:@"arrangedObjects"
2188 [self bindSwitch:matrix cmdline:@"-text-mode %"];
2190 [self placeChild:matrix on:group];
2191 [self placeChild:rgroup on:group right:YES];
2195 # else // USE_IPHONE
2197 NSView *rgroup = parent;
2200 // <select id="textMode">
2201 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
2202 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
2203 // <option id="url" _label="Display URL"/>
2206 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2207 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2209 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2210 [node3 setAttributesAsDictionary:
2212 @"arg-set": @"-text-mode date",
2213 @"_label": @"Display the date and time" }];
2214 [node3 setParent: node2];
2217 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2218 [node3 setAttributesAsDictionary:
2220 @"arg-set": @"-text-mode literal",
2221 @"_label": @"Display static text" }];
2222 [node3 setParent: node2];
2225 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2226 [node3 setAttributesAsDictionary:
2228 @"_label": @"Display the contents of a URL" }];
2229 [node3 setParent: node2];
2232 [self makeOptionMenu:node2 on:rgroup];
2234 # endif // USE_IPHONE
2237 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2238 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2239 [node2 setAttributesAsDictionary:
2240 @{ @"id": @"textLiteral",
2241 @"arg": @"-text-literal %",
2243 @"_label": @"Text to display"
2246 [self makeTextField:node2 on:rgroup
2254 // rect = [last_child(rgroup) frame];
2256 /* // trying to make the text fields be enabled only when the checkbox is on..
2257 control = last_child (rgroup);
2258 [control bind:@"enabled"
2259 toObject:[matrix cellAtRow:1 column:0]
2260 withKeyPath:@"value"
2266 // <file id="textFile" _label="" arg-set="-text-file %"/>
2267 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2268 [node2 setAttributesAsDictionary:
2269 @{ @"id": @"textFile",
2270 @"arg": @"-text-file %" }];
2271 [self makeFileSelector:node2 on:rgroup
2272 dirsOnly:NO withLabel:NO editable:NO];
2273 # endif // !USE_IPHONE
2275 // rect = [last_child(rgroup) frame];
2277 // <string id="textURL" _label="" arg-set="text-url %"/>
2278 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2279 [node2 setAttributesAsDictionary:
2280 @{ @"id": @"textURL",
2281 @"arg": @"-text-url %",
2283 @"_label": @"URL to display",
2286 [self makeTextField:node2 on:rgroup
2294 // rect = [last_child(rgroup) frame];
2298 // <string id="textProgram" _label="" arg-set="text-program %"/>
2299 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2300 [node2 setAttributesAsDictionary:
2301 @{ @"id": @"textProgram",
2302 @"arg": @"-text-program %",
2304 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2307 // rect = [last_child(rgroup) frame];
2309 layout_group (rgroup, NO);
2311 rect = [rgroup frame];
2312 rect.size.width += 35; // WTF? Why is rgroup too narrow?
2313 [rgroup setFrame:rect];
2316 // Set the height of the cells in the radio-box matrix to the height of
2317 // the (last of the) text fields.
2318 control = last_child (rgroup);
2319 rect = [control frame];
2320 rect.size.width = 30; // width of the string "Text", plus a bit...
2322 rect.size.width += 25;
2323 rect.size.height += LINE_SPACING;
2324 [matrix setCellSize:rect.size];
2325 [matrix sizeToCells];
2327 layout_group (group, YES);
2328 rect = [matrix frame];
2329 rect.origin.x += rect.size.width + COLUMN_SPACING;
2330 rect.origin.y -= [control frame].size.height - LINE_SPACING;
2331 [rgroup setFrameOrigin:rect.origin];
2333 // now cheat on the size of the matrix: allow it to overlap (underlap)
2336 rect.size = [matrix cellSize];
2337 rect.size.width = 300;
2338 [matrix setCellSize:rect.size];
2339 [matrix sizeToCells];
2341 // Cheat on the position of the stuff on the right (the rgroup).
2342 // GAAAH, this code is such crap!
2343 rect = [rgroup frame];
2345 [rgroup setFrame:rect];
2348 rect.size.width = rect.size.height = 0;
2349 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2350 [box setTitlePosition:NSAtTop];
2351 [box setBorderType:NSBezelBorder];
2352 [box setTitle:@"Display Text"];
2354 rect.size.width = rect.size.height = 12;
2355 [box setContentViewMargins:rect.size];
2356 [box setContentView:group];
2359 [self placeChild:box on:parent];
2361 # endif // !USE_IPHONE
2365 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2368 [x] Grab desktop images
2369 [ ] Choose random image:
2370 [__________________________] [Choose]
2372 <boolean id="grabDesktopImages" _label="Grab desktop images"
2373 arg-unset="-no-grab-desktop"/>
2374 <boolean id="chooseRandomImages" _label="Grab desktop images"
2375 arg-unset="-choose-random-images"/>
2376 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2379 NSXMLElement *node2;
2382 # define SCREENS "Grab desktop images"
2383 # define PHOTOS "Choose random images"
2385 # define SCREENS "Grab screenshots"
2386 # define PHOTOS "Use photo library"
2389 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2390 [node2 setAttributesAsDictionary:
2391 @{ @"id": @"grabDesktopImages",
2392 @"_label": @ SCREENS,
2393 @"arg-unset": @"-no-grab-desktop",
2395 [self makeCheckbox:node2 on:parent];
2397 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2398 [node2 setAttributesAsDictionary:
2399 @{ @"id": @"chooseRandomImages",
2400 @"_label": @ PHOTOS,
2401 @"arg-set": @"-choose-random-images",
2403 [self makeCheckbox:node2 on:parent];
2405 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2406 [node2 setAttributesAsDictionary:
2407 @{ @"id": @"imageDirectory",
2408 @"_label": @"Images from:",
2409 @"arg": @"-image-directory %",
2411 [self makeFileSelector:node2 on:parent
2412 dirsOnly:YES withLabel:YES editable:YES];
2418 // Add a second, explanatory label below the file/URL selector.
2421 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2422 [self placeChild:lab2 on:parent];
2424 // Pack it in a little tighter vertically.
2425 NSRect r2 = [lab2 frame];
2428 [lab2 setFrameOrigin:r2.origin];
2430 # endif // USE_IPHONE
2434 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2438 [x] Check for Updates [ Monthly ]
2441 <boolean id="automaticallyChecksForUpdates"
2442 _label="Automatically check for updates"
2443 arg-unset="-no-automaticallyChecksForUpdates" />
2444 <select id="updateCheckInterval">
2445 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2446 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/>
2447 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2448 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2456 rect.size.width = rect.size.height = 1;
2457 rect.origin.x = rect.origin.y = 0;
2458 NSView *group = [[NSView alloc] initWithFrame:rect];
2460 NSXMLElement *node2;
2464 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2465 [node2 setAttributesAsDictionary:
2466 @{ @"id": @SUSUEnableAutomaticChecksKey,
2467 @"_label": @"Automatically check for updates",
2468 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2470 [self makeCheckbox:node2 on:group];
2474 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2475 [node2 setAttributesAsDictionary:
2476 @{ @"id": @SUScheduledCheckIntervalKey }];
2480 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2481 [node3 setAttributesAsDictionary:
2482 @{ @"id": @"hourly",
2483 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2484 @"_label": @"Hourly" }];
2485 [node3 setParent: node2];
2488 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2489 [node3 setAttributesAsDictionary:
2491 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2492 @"_label": @"Daily" }];
2493 [node3 setParent: node2];
2496 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2497 [node3 setAttributesAsDictionary:
2498 @{ @"id": @"weekly",
2499 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2500 @"_label": @"Weekly",
2502 [node3 setParent: node2];
2505 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2506 [node3 setAttributesAsDictionary:
2507 @{ @"id": @"monthly",
2508 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2509 @"_label": @"Monthly",
2511 [node3 setParent: node2];
2515 [self makeOptionMenu:node2 on:group];
2518 layout_group (group, TRUE);
2520 rect.size.width = rect.size.height = 0;
2521 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2522 [box setTitlePosition:NSNoTitle];
2523 [box setBorderType:NSNoBorder];
2524 [box setContentViewMargins:rect.size];
2525 [box setContentView:group];
2528 [self placeChild:box on:parent];
2533 # endif // !USE_IPHONE
2537 #pragma mark Layout for controls
2542 last_child (NSView *parent)
2544 NSArray *kids = [parent subviews];
2545 int nkids = [kids count];
2549 return [kids objectAtIndex:nkids-1];
2551 #endif // USE_IPHONE
2554 /* Add the child as a subview of the parent, positioning it immediately
2555 below or to the right of the previously-added child of that view.
2557 - (void) placeChild:
2563 on:(NSView *)parent right:(BOOL)right_p
2566 NSRect rect = [child frame];
2567 NSView *last = last_child (parent);
2569 rect.origin.x = LEFT_MARGIN;
2570 rect.origin.y = ([parent frame].size.height - rect.size.height
2572 } else if (right_p) {
2573 rect = [last frame];
2574 rect.origin.x += rect.size.width + COLUMN_SPACING;
2576 rect = [last frame];
2577 rect.origin.x = LEFT_MARGIN;
2578 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2580 NSRect r = [child frame];
2581 r.origin = rect.origin;
2583 [parent addSubview:child];
2585 # else // USE_IPHONE
2587 /* Controls is an array of arrays of the controls, divided into sections.
2588 Each hgroup / vgroup gets a nested array, too, e.g.:
2590 [ [ [ <label>, <checkbox> ],
2591 [ <label>, <checkbox> ],
2592 [ <label>, <checkbox> ] ],
2593 [ <label>, <text-field> ],
2594 [ <label>, <low-label>, <slider>, <high-label> ],
2595 [ <low-label>, <slider>, <high-label> ],
2599 If an element begins with a label, it is terminal, otherwise it is a
2600 group. There are (currently) never more than 4 elements in a single
2603 A blank vertical spacer is placed between each hgroup / vgroup,
2604 by making each of those a new section in the TableView.
2607 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2608 if ([controls count] == 0)
2609 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2610 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2612 if (!right_p || [current count] == 0) {
2613 // Nothing on the current line. Add this object.
2614 [current addObject: child];
2616 // Something's on the current line already.
2617 NSObject *old = [current objectAtIndex:[current count]-1];
2618 if ([old isKindOfClass:[NSMutableArray class]]) {
2619 // Already an array in this cell. Append.
2620 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2621 [(NSMutableArray *) old addObject: child];
2623 // Replace the control in this cell with an array, then append
2624 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2625 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2628 # endif // USE_IPHONE
2632 - (void) placeChild:(NSView *)child on:(NSView *)parent
2634 [self placeChild:child on:parent right:NO];
2640 // Start putting subsequent children in a new group, to create a new
2641 // section on the UITableView.
2643 - (void) placeSeparator
2645 if (! controls) return;
2646 if ([controls count] == 0) return;
2647 if ([[controls objectAtIndex:[controls count]-1]
2649 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2651 #endif // USE_IPHONE
2655 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2656 wrapped in <hgroup> or <vgroup> in the XML.
2658 - (void) makeGroup:(NSXMLNode *)node
2660 horizontal:(BOOL) horiz_p
2663 if (!horiz_p) [self placeSeparator];
2664 [self traverseChildren:node on:parent];
2665 if (!horiz_p) [self placeSeparator];
2666 # else // !USE_IPHONE
2668 rect.size.width = rect.size.height = 1;
2669 rect.origin.x = rect.origin.y = 0;
2670 NSView *group = [[NSView alloc] initWithFrame:rect];
2671 [self traverseChildren:node on:group];
2673 layout_group (group, horiz_p);
2675 rect.size.width = rect.size.height = 0;
2676 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2677 [box setTitlePosition:NSNoTitle];
2678 [box setBorderType:NSNoBorder];
2679 [box setContentViewMargins:rect.size];
2680 [box setContentView:group];
2683 [self placeChild:box on:parent];
2684 # endif // !USE_IPHONE
2690 layout_group (NSView *group, BOOL horiz_p)
2692 NSArray *kids = [group subviews];
2693 int nkids = [kids count];
2695 double maxx = 0, miny = 0;
2696 for (i = 0; i < nkids; i++) {
2697 NSView *kid = [kids objectAtIndex:i];
2698 NSRect r = [kid frame];
2701 maxx += r.size.width + COLUMN_SPACING;
2702 if (r.size.height > -miny) miny = -r.size.height;
2704 if (r.size.width > maxx) maxx = r.size.width;
2705 miny = r.origin.y - r.size.height;
2712 rect.size.width = maxx;
2713 rect.size.height = -miny;
2714 [group setFrame:rect];
2717 for (i = 0; i < nkids; i++) {
2718 NSView *kid = [kids objectAtIndex:i];
2719 NSRect r = [kid frame];
2721 r.origin.y = rect.size.height - r.size.height;
2723 x += r.size.width + COLUMN_SPACING;
2730 #endif // !USE_IPHONE
2733 /* Create some kind of control corresponding to the given XML node.
2735 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2737 NSString *name = [node name];
2739 if ([node kind] == NSXMLCommentKind)
2742 if ([node kind] == NSXMLTextKind) {
2743 NSString *s = [(NSString *) [node objectValue]
2744 stringByTrimmingCharactersInSet:
2745 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2746 if (! [s isEqualToString:@""]) {
2747 NSAssert1 (0, @"unexpected text: %@", s);
2752 if ([node kind] != NSXMLElementKind) {
2753 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2757 if ([name isEqualToString:@"hgroup"] ||
2758 [name isEqualToString:@"vgroup"]) {
2760 [self makeGroup:node on:parent
2761 horizontal:[name isEqualToString:@"hgroup"]];
2763 } else if ([name isEqualToString:@"command"]) {
2764 // do nothing: this is the "-root" business
2766 } else if ([name isEqualToString:@"video"]) {
2769 } else if ([name isEqualToString:@"boolean"]) {
2770 [self makeCheckbox:node on:parent];
2772 } else if ([name isEqualToString:@"string"]) {
2773 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2775 } else if ([name isEqualToString:@"file"]) {
2776 [self makeFileSelector:node on:parent
2777 dirsOnly:NO withLabel:YES editable:NO];
2779 } else if ([name isEqualToString:@"number"]) {
2780 [self makeNumberSelector:node on:parent];
2782 } else if ([name isEqualToString:@"select"]) {
2783 [self makeOptionMenu:node on:parent];
2785 } else if ([name isEqualToString:@"_description"]) {
2786 [self makeDescLabel:node on:parent];
2788 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2789 [self makeTextLoaderControlBox:node on:parent];
2791 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2792 [self makeImageLoaderControlBox:node on:parent];
2794 } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2795 [self makeUpdaterControlBox:node on:parent];
2798 NSAssert1 (0, @"unknown tag: %@", name);
2803 /* Iterate over and process the children of this XML node.
2805 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2807 NSArray *children = [node children];
2808 NSUInteger i, count = [children count];
2809 for (i = 0; i < count; i++) {
2810 NSXMLNode *child = [children objectAtIndex:i];
2811 [self makeControl:child on:parent];
2818 /* Kludgey magic to make the window enclose the controls we created.
2821 fix_contentview_size (NSView *parent)
2824 NSArray *kids = [parent subviews];
2825 int nkids = [kids count];
2826 NSView *text = 0; // the NSText at the bottom of the window
2827 double maxx = 0, miny = 0;
2830 /* Find the size of the rectangle taken up by each of the children
2831 except the final "NSText" child.
2833 for (i = 0; i < nkids; i++) {
2834 NSView *kid = [kids objectAtIndex:i];
2835 if ([kid isKindOfClass:[NSText class]]) {
2840 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2841 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2842 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2843 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2844 // f.origin.y + f.size.height, [kid class]);
2847 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2849 /* Now that we know the width of the window, set the width of the NSText to
2850 that, so that it can decide what its height needs to be.
2852 if (! text) abort();
2854 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2855 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2856 // f.origin.y + f.size.height, [text class]);
2858 // set the NSText's width (this changes its height).
2859 f.size.width = maxx - LEFT_MARGIN;
2862 // position the NSText below the last child (this gives us a new miny).
2864 f.origin.y = miny - f.size.height - LINE_SPACING;
2865 miny = f.origin.y - LINE_SPACING;
2868 // Lock the width of the field and unlock the height, and let it resize
2869 // once more, to compute the proper height of the text for that width.
2871 [(NSText *) text setHorizontallyResizable:NO];
2872 [(NSText *) text setVerticallyResizable:YES];
2873 [(NSText *) text sizeToFit];
2875 // Now lock the height too: no more resizing this text field.
2877 [(NSText *) text setVerticallyResizable:NO];
2879 // Now reposition the top edge of the text field to be back where it
2880 // was before we changed the height.
2882 float oh = f.size.height;
2884 float dh = f.size.height - oh;
2887 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2888 // If we do this in 10.6, the text field moves down, off the window.
2889 // So instead we repair it at the end, at the "WTF2" comment.
2892 // Also adjust the parent height by the change in height of the text field.
2895 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2896 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2897 // f.origin.y + f.size.height, [text class]);
2900 /* Set the contentView to the size of the children.
2903 // float yoff = f.size.height;
2904 f.size.width = maxx + LEFT_MARGIN;
2905 f.size.height = -(miny - LEFT_MARGIN*2);
2906 // yoff = f.size.height - yoff;
2907 [parent setFrame:f];
2909 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2910 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2912 /* Now move all of the kids up into the window.
2915 float shift = f.size.height;
2916 // NSLog(@"shift: %3.0f", shift);
2917 for (i = 0; i < nkids; i++) {
2918 NSView *kid = [kids objectAtIndex:i];
2920 f.origin.y += shift;
2922 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2923 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2924 // f.origin.y + f.size.height, [kid class]);
2929 parent: 420 x 541 @ 0 0
2930 text: 380 x 100 @ 20 22 miny=-501
2933 parent: 420 x 541 @ 0 0
2934 text: 380 x 100 @ 20 50 miny=-501
2937 // #### WTF2: See "WTF" above. If the text field is off the screen,
2938 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2941 if (f.origin.y < 50) { // magic numbers, yay
2946 /* Set the kids to track the top left corner of the window when resized.
2947 Set the NSText to track the bottom right corner as well.
2949 for (i = 0; i < nkids; i++) {
2950 NSView *kid = [kids objectAtIndex:i];
2951 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2952 if ([kid isKindOfClass:[NSText class]])
2953 mask |= NSViewWidthSizable|NSViewHeightSizable;
2954 [kid setAutoresizingMask:mask];
2957 # endif // !USE_IPHONE
2963 wrap_with_buttons (NSWindow *window, NSView *panel)
2967 // Make a box to hold the buttons at the bottom of the window.
2969 rect = [panel frame];
2970 rect.origin.x = rect.origin.y = 0;
2971 rect.size.height = 10;
2972 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2973 [bbox setTitlePosition:NSNoTitle];
2974 [bbox setBorderType:NSNoBorder];
2976 // Make some buttons: Default, Cancel, OK
2978 rect.origin.x = rect.origin.y = 0;
2979 rect.size.width = rect.size.height = 10;
2980 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
2981 [reset setTitle:@"Reset to Defaults"];
2982 [reset setBezelStyle:NSRoundedBezelStyle];
2985 rect = [reset frame];
2986 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
2987 [ok setTitle:@"OK"];
2988 [ok setBezelStyle:NSRoundedBezelStyle];
2990 rect = [bbox frame];
2991 rect.origin.x = rect.size.width - [ok frame].size.width;
2992 [ok setFrameOrigin:rect.origin];
2995 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
2996 [cancel setTitle:@"Cancel"];
2997 [cancel setBezelStyle:NSRoundedBezelStyle];
2999 rect.origin.x -= [cancel frame].size.width + 10;
3000 [cancel setFrameOrigin:rect.origin];
3002 // Bind OK to RET and Cancel to ESC.
3003 [ok setKeyEquivalent:@"\r"];
3004 [cancel setKeyEquivalent:@"\e"];
3006 // The correct width for OK and Cancel buttons is 68 pixels
3007 // ("Human Interface Guidelines: Controls: Buttons:
3008 // Push Button Specifications").
3011 rect.size.width = 68;
3014 rect = [cancel frame];
3015 rect.size.width = 68;
3016 [cancel setFrame:rect];
3018 // It puts the buttons in the box or else it gets the hose again
3020 [bbox addSubview:ok];
3021 [bbox addSubview:cancel];
3022 [bbox addSubview:reset];
3025 // make a box to hold the button-box, and the preferences view
3027 rect = [bbox frame];
3028 rect.origin.y += rect.size.height;
3029 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
3030 [pbox setTitlePosition:NSNoTitle];
3031 [pbox setBorderType:NSBezelBorder];
3033 // Enforce a max height on the dialog, so that it's obvious to me
3034 // (on a big screen) when the dialog will fall off the bottom of
3035 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
3037 NSRect f = [panel frame];
3038 int screen_height = (768 // shortest "modern" Mac display
3040 - 56 // System Preferences toolbar
3041 - 140 // default magnified bottom dock icon
3043 if (f.size.height > screen_height) {
3044 NSLog(@"%@ height was %.0f; clipping to %d",
3045 [panel class], f.size.height, screen_height);
3046 f.size.height = screen_height;
3051 [pbox addSubview:panel];
3052 [pbox addSubview:bbox];
3055 [reset setAutoresizingMask:NSViewMaxXMargin];
3056 [cancel setAutoresizingMask:NSViewMinXMargin];
3057 [ok setAutoresizingMask:NSViewMinXMargin];
3058 [bbox setAutoresizingMask:NSViewWidthSizable];
3062 [ok setTarget:window];
3063 [cancel setTarget:window];
3064 [reset setTarget:window];
3065 [ok setAction:@selector(okAction:)];
3066 [cancel setAction:@selector(cancelAction:)];
3067 [reset setAction:@selector(resetAction:)];
3073 #endif // !USE_IPHONE
3076 /* Iterate over and process the children of the root node of the XML document.
3078 - (void)traverseTree
3081 NSView *parent = [self view];
3083 NSWindow *parent = self;
3085 NSXMLNode *node = xml_root;
3087 if (![[node name] isEqualToString:@"screensaver"]) {
3088 NSAssert (0, @"top level node is not <xscreensaver>");
3091 saver_name = [self parseXScreenSaverTag: node];
3092 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
3094 [saver_name retain];
3099 rect.origin.x = rect.origin.y = 0;
3100 rect.size.width = rect.size.height = 1;
3102 NSView *panel = [[NSView alloc] initWithFrame:rect];
3103 [self traverseChildren:node on:panel];
3104 fix_contentview_size (panel);
3106 NSView *root = wrap_with_buttons (parent, panel);
3107 [userDefaultsController setAppliesImmediately:NO];
3108 [globalDefaultsController setAppliesImmediately:NO];
3110 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
3112 rect = [parent frameRectForContentRect:[root frame]];
3113 [parent setFrame:rect display:NO];
3114 [parent setMinSize:rect.size];
3116 [parent setContentView:root];
3121 # else // USE_IPHONE
3123 CGRect r = [parent frame];
3124 r.size = [[UIScreen mainScreen] bounds].size;
3125 [parent setFrame:r];
3126 [self traverseChildren:node on:parent];
3128 # endif // USE_IPHONE
3132 - (void)parser:(NSXMLParser *)parser
3133 didStartElement:(NSString *)elt
3134 namespaceURI:(NSString *)ns
3135 qualifiedName:(NSString *)qn
3136 attributes:(NSDictionary *)attrs
3138 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];
3176 # ifdef USE_PICKER_VIEW
3178 #pragma mark UIPickerView delegate methods
3180 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3182 return 1; // Columns
3185 - (NSInteger)pickerView:(UIPickerView *)pv
3186 numberOfRowsInComponent:(NSInteger)column
3188 NSAssert (column == 0, @"weird column");
3189 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3190 if (! a) return 0; // Too early?
3194 - (CGFloat)pickerView:(UIPickerView *)pv
3195 rowHeightForComponent:(NSInteger)column
3200 - (CGFloat)pickerView:(UIPickerView *)pv
3201 widthForComponent:(NSInteger)column
3203 NSAssert (column == 0, @"weird column");
3204 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3205 if (! a) return 0; // Too early?
3207 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3209 for (NSArray *a2 in a) {
3210 NSString *s = [a2 objectAtIndex:0];
3211 CGSize r = [s sizeWithFont:f];
3212 if (r.width > max) max = r.width;
3215 max *= 1.7; // WTF!!
3227 - (NSString *)pickerView:(UIPickerView *)pv
3228 titleForRow:(NSInteger)row
3229 forComponent:(NSInteger)column
3231 NSAssert (column == 0, @"weird column");
3232 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3233 if (! a) return 0; // Too early?
3234 a = [a objectAtIndex:row];
3235 NSAssert (a, @"internal error");
3236 return [a objectAtIndex:0];
3239 # endif // USE_PICKER_VIEW
3242 #pragma mark UITableView delegate methods
3244 - (void) addResetButton
3246 [[self navigationItem]
3247 setRightBarButtonItem: [[UIBarButtonItem alloc]
3248 initWithTitle: @"Reset to Defaults"
3249 style: UIBarButtonItemStyleBordered
3251 action:@selector(resetAction:)]];
3252 NSString *s = saver_name;
3253 if ([self view].frame.size.width > 320)
3254 s = [s stringByAppendingString: @" Settings"];
3255 [self navigationItem].title = s;
3259 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3264 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3265 // Number of vertically-stacked white boxes.
3266 return [controls count];
3269 - (NSInteger)tableView:(UITableView *)tableView
3270 numberOfRowsInSection:(NSInteger)section
3272 // Number of lines in each vertically-stacked white box.
3273 NSAssert (controls, @"internal error");
3274 return [[controls objectAtIndex:section] count];
3277 - (NSString *)tableView:(UITableView *)tv
3278 titleForHeaderInSection:(NSInteger)section
3280 // Titles above each vertically-stacked white box.
3281 // if (section == 0)
3282 // return [saver_name stringByAppendingString:@" Settings"];
3287 - (CGFloat)tableView:(UITableView *)tv
3288 heightForRowAtIndexPath:(NSIndexPath *)ip
3292 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3293 objectAtIndex:[ip indexAtPosition:1]];
3295 if ([ctl isKindOfClass:[NSArray class]]) {
3296 NSArray *set = (NSArray *) ctl;
3297 switch ([set count]) {
3298 case 4: // label + left/slider/right.
3299 case 3: // left/slider/right.
3300 h = FONT_SIZE * 3.0;
3302 case 2: // Checkboxes, or text fields.
3303 h = FONT_SIZE * 2.4;
3306 } else if ([ctl isKindOfClass:[UILabel class]]) {
3307 // Radio buttons in a multi-select list.
3308 h = FONT_SIZE * 1.9;
3310 # ifdef USE_HTML_LABELS
3311 } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3313 HTMLLabel *t = (HTMLLabel *) ctl;
3315 r.size.width = [tv frame].size.width;
3316 r.size.width -= LEFT_MARGIN * 2;
3321 # endif // USE_HTML_LABELS
3323 } else { // Does this ever happen?
3324 h = FONT_SIZE + LINE_SPACING * 2;
3327 if (h <= 0) abort();
3332 - (void)refreshTableView
3334 UITableView *tv = (UITableView *) [self view];
3335 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3336 NSInteger rows = [self numberOfSectionsInTableView:tv];
3337 for (int i = 0; i < rows; i++) {
3338 NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3339 for (int j = 0; j < cols; j++) {
3343 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3348 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3351 // Default opacity looks bad.
3352 // #### Oh great, this only works *sometimes*.
3353 UIView *v = [[self navigationItem] titleView];
3354 [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
3358 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3360 [NSTimer scheduledTimerWithTimeInterval: 0
3362 selector:@selector(refreshTableView)
3368 #ifndef USE_PICKER_VIEW
3370 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3371 button:(RadioButton *)b
3373 NSArray *item = [[b items] objectAtIndex: [b index]];
3374 NSString *pref_key = [item objectAtIndex:1];
3375 NSObject *pref_val = [item objectAtIndex:2];
3377 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3379 // Convert them both to strings and compare those, so that
3380 // we don't get screwed by int 1 versus string "1".
3381 // Will boolean true/1 screw us here too?
3383 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3384 ? (NSString *) pref_val
3385 : [(NSNumber *) pref_val stringValue]);
3386 NSString *current_str = ([current isKindOfClass:[NSString class]]
3387 ? (NSString *) current
3388 : [(NSNumber *) current stringValue]);
3389 BOOL match_p = [current_str isEqualToString:pref_str];
3391 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3394 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3396 [cell setAccessoryType:UITableViewCellAccessoryNone];
3400 - (void)tableView:(UITableView *)tv
3401 didSelectRowAtIndexPath:(NSIndexPath *)ip
3403 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3404 objectAtIndex:[ip indexAtPosition:1]];
3405 if (! [ctl isKindOfClass:[RadioButton class]])
3408 [self radioAction:ctl];
3409 [self refreshTableView];
3413 #endif // !USE_PICKER_VIEW
3417 - (UITableViewCell *)tableView:(UITableView *)tv
3418 cellForRowAtIndexPath:(NSIndexPath *)ip
3420 CGFloat ww = [tv frame].size.width;
3421 CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
3423 float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
3425 // Width of the column of labels on the left.
3426 CGFloat left_width = ww * 0.4;
3427 CGFloat right_edge = ww - LEFT_MARGIN;
3429 if (os_version < 7) // margins were wider on iOS 6.1
3432 CGFloat max = FONT_SIZE * 12;
3433 if (left_width > max) left_width = max;
3435 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3436 objectAtIndex:[ip indexAtPosition:1]];
3438 if ([ctl isKindOfClass:[NSArray class]]) {
3439 // This cell has a set of objects in it.
3440 NSArray *set = (NSArray *) ctl;
3441 switch ([set count]) {
3444 // With 2 elements, the first of the pair must be a label.
3445 UILabel *label = (UILabel *) [set objectAtIndex: 0];
3446 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3447 ctl = [set objectAtIndex: 1];
3449 CGRect r = [ctl frame];
3451 if ([ctl isKindOfClass:[UISwitch class]]) { // Checkboxes.
3452 r.size.width = 80; // Magic.
3453 r.origin.x = right_edge - r.size.width + 30; // beats me
3455 if (os_version < 7) // checkboxes were wider on iOS 6.1
3459 r.origin.x = left_width; // Text fields, etc.
3460 r.size.width = right_edge - r.origin.x;
3463 r.origin.y = (hh - r.size.height) / 2; // Center vertically.
3466 // Make a box and put the label and checkbox/slider into it.
3471 NSView *box = [[UIView alloc] initWithFrame:r];
3472 [box addSubview: ctl];
3474 // Let the label make use of any space not taken up by the control.
3476 r.origin.x = LEFT_MARGIN;
3478 r.size.width = [ctl frame].origin.x - r.origin.x;
3481 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3482 [box addSubview: label];
3490 // With 3 elements, 1 and 3 are labels.
3491 // With 4 elements, 1, 2 and 4 are labels.
3493 UILabel *top = ([set count] == 4
3494 ? [set objectAtIndex: i++]
3496 UILabel *left = [set objectAtIndex: i++];
3497 NSView *mid = [set objectAtIndex: i++];
3498 UILabel *right = [set objectAtIndex: i++];
3499 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3500 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3501 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3502 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3504 // 3 elements: control at top of cell.
3505 // 4 elements: center the control vertically.
3506 CGRect r = [mid frame];
3507 r.size.height = 32; // Unchangable height of the slider thumb.
3509 // Center the slider between left_width and right_edge.
3510 # ifdef LABEL_ABOVE_SLIDER
3511 r.origin.x = LEFT_MARGIN;
3513 r.origin.x = left_width;
3515 r.origin.y = (hh - r.size.height) / 2;
3516 r.size.width = right_edge - r.origin.x;
3520 r.size = [[top text] sizeWithFont:[top font]
3522 CGSizeMake (ww - LEFT_MARGIN*2, 100000)
3523 lineBreakMode:[top lineBreakMode]];
3524 # ifdef LABEL_ABOVE_SLIDER
3525 // Top label goes above, flush center/top.
3526 r.origin.x = (ww - r.size.width) / 2;
3528 # else // !LABEL_ABOVE_SLIDER
3529 // Label goes on the left.
3530 r.origin.x = LEFT_MARGIN;
3532 r.size.width = left_width - LEFT_MARGIN;
3534 # endif // !LABEL_ABOVE_SLIDER
3538 // Left label goes under control, flush left/bottom.
3539 r.size = [[left text] sizeWithFont:[left font]
3541 CGSizeMake(ww - LEFT_MARGIN*2, 100000)
3542 lineBreakMode:[left lineBreakMode]];
3543 r.origin.x = [mid frame].origin.x;
3544 r.origin.y = hh - r.size.height - 4;
3547 // Right label goes under control, flush right/bottom.
3549 r.size = [[right text] sizeWithFont:[right font]
3551 CGSizeMake(ww - LEFT_MARGIN*2, 1000000)
3552 lineBreakMode:[right lineBreakMode]];
3553 r.origin.x = ([mid frame].origin.x + [mid frame].size.width -
3555 r.origin.y = [left frame].origin.y;
3558 // Make a box and put the labels and slider into it.
3563 NSView *box = [[UIView alloc] initWithFrame:r];
3565 [box addSubview: top];
3566 [box addSubview: left];
3567 [box addSubview: right];
3568 [box addSubview: mid];
3574 NSAssert (0, @"unhandled size");
3576 } else { // A single view, not a pair.
3577 CGRect r = [ctl frame];
3578 r.origin.x = LEFT_MARGIN;
3580 r.size.width = right_edge - r.origin.x;
3585 NSString *id = @"Cell";
3586 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
3588 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3589 reuseIdentifier: id]
3592 for (UIView *subview in [cell.contentView subviews])
3593 [subview removeFromSuperview];
3594 [cell.contentView addSubview: ctl];
3595 CGRect r = [ctl frame];
3599 cell.selectionStyle = UITableViewCellSelectionStyleNone;
3600 [cell setAccessoryType:UITableViewCellAccessoryNone];
3602 # ifndef USE_PICKER_VIEW
3603 if ([ctl isKindOfClass:[RadioButton class]])
3604 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3605 # endif // USE_PICKER_VIEW
3609 # endif // USE_IPHONE
3612 /* When this object is instantiated, it parses the XML file and creates
3613 controls on itself that are hooked up to the appropriate preferences.
3614 The default size of the view is just big enough to hold them all.
3616 - (id)initWithXML: (NSData *) xml_data
3617 options: (const XrmOptionDescRec *) _opts
3618 controller: (NSUserDefaultsController *) _prefs
3619 globalController: (NSUserDefaultsController *) _globalPrefs
3620 defaults: (NSDictionary *) _defs
3623 self = [super init];
3624 # else // !USE_IPHONE
3625 self = [super initWithStyle:UITableViewStyleGrouped];
3626 self.title = [saver_name stringByAppendingString:@" Settings"];
3627 # endif // !USE_IPHONE
3628 if (! self) return 0;
3630 // instance variables
3632 defaultOptions = _defs;
3633 userDefaultsController = [_prefs retain];
3634 globalDefaultsController = [_globalPrefs retain];
3636 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3639 NSAssert1 (0, @"XML Error: %@",
3640 [[NSString alloc] initWithData:xml_data
3641 encoding:NSUTF8StringEncoding]);
3644 [xmlDoc setDelegate:self];
3645 if (! [xmlDoc parse]) {
3646 NSError *err = [xmlDoc parserError];
3647 NSAssert2 (0, @"XML Error: %@: %@",
3648 [[NSString alloc] initWithData:xml_data
3649 encoding:NSUTF8StringEncoding],
3655 TextModeTransformer *t = [[TextModeTransformer alloc] init];
3656 [NSValueTransformer setValueTransformer:t
3657 forName:@"TextModeTransformer"];
3658 # endif // USE_IPHONE
3660 [self traverseTree];
3664 [self addResetButton];
3673 [saver_name release];
3674 [userDefaultsController release];
3675 [globalDefaultsController release];
3678 [pref_keys release];
3679 [pref_ctls release];
3680 # ifdef USE_PICKER_VIEW
3681 [picker_values release];