1 /* xscreensaver, Copyright (c) 2006-2017 Jamie Zawinski <jwz@jwz.org>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
12 /* XScreenSaver uses XML files to describe the user interface for configuring
13 the various screen savers. These files live in .../hacks/config/ and
14 say relatively high level things like: "there should be a checkbox
15 labelled "Leave Trails", and when it is checked, add the option '-trails'
16 to the command line when launching the program."
18 This code reads that XML and constructs a Cocoa interface from it.
19 The Cocoa controls are hooked up to NSUserDefaultsController to save
20 those settings into the MacOS preferences system. The Cocoa preferences
21 names are the same as the resource names specified in the screenhack's
22 'options' array (we use that array to map the command line switches
23 specified in the XML to the resource names to use).
26 #import "XScreenSaverConfigSheet.h"
30 #import "InvertedSlider.h"
33 # define NSView UIView
34 # define NSRect CGRect
35 # define NSSize CGSize
36 # define NSTextField UITextField
37 # define NSButton UIButton
38 # define NSFont UIFont
39 # define NSStepper UIStepper
40 # define NSMenuItem UIMenuItem
41 # define NSText UILabel
42 # define minValue minimumValue
43 # define maxValue maximumValue
44 # define setMinValue setMinimumValue
45 # define setMaxValue setMaximumValue
46 # define LABEL UILabel
48 # define LABEL NSTextField
51 #undef LABEL_ABOVE_SLIDER
52 #define USE_HTML_LABELS
55 #pragma mark XML Parser
57 /* I used to use the "NSXMLDocument" XML parser, but that doesn't exist
58 on iOS. The "NSXMLParser" parser exists on both OSX and iOS, so I
59 converted to use that. However, to avoid having to re-write all of
60 the old code, I faked out a halfassed implementation of the
61 "NSXMLNode" class that "NSXMLDocument" used to return.
64 #define NSXMLNode SimpleXMLNode
65 #define NSXMLElement SimpleXMLNode
66 #define NSXMLCommentKind SimpleXMLCommentKind
67 #define NSXMLElementKind SimpleXMLElementKind
68 #define NSXMLAttributeKind SimpleXMLAttributeKind
69 #define NSXMLTextKind SimpleXMLTextKind
71 typedef enum { SimpleXMLCommentKind,
73 SimpleXMLAttributeKind,
77 @interface SimpleXMLNode : NSObject
81 SimpleXMLNode *parent;
82 NSMutableArray *children;
83 NSMutableArray *attributes;
87 @property(nonatomic) SimpleXMLKind kind;
88 @property(nonatomic, retain) NSString *name;
89 @property(nonatomic, retain) SimpleXMLNode *parent;
90 @property(nonatomic, retain) NSMutableArray *children;
91 @property(nonatomic, retain) NSMutableArray *attributes;
92 @property(nonatomic, retain, getter=objectValue, setter=setObjectValue:)
97 @implementation SimpleXMLNode
101 //@synthesize parent;
102 @synthesize children;
103 @synthesize attributes;
109 attributes = [NSMutableArray arrayWithCapacity:10];
114 - (id) initWithName:(NSString *)n
117 [self setKind:NSXMLElementKind];
123 - (void) setAttributesAsDictionary:(NSDictionary *)dict
125 for (NSString *key in dict) {
126 NSObject *val = [dict objectForKey:key];
127 SimpleXMLNode *n = [[SimpleXMLNode alloc] init];
128 [n setKind:SimpleXMLAttributeKind];
130 [n setObjectValue:val];
131 [attributes addObject:n];
136 - (SimpleXMLNode *) parent { return parent; }
138 - (void) setParent:(SimpleXMLNode *)p
140 NSAssert (!parent, @"parent already set");
143 NSMutableArray *kids = [p children];
145 kids = [NSMutableArray arrayWithCapacity:10];
146 [p setChildren:kids];
148 [kids addObject:self];
153 #pragma mark textMode value transformer
155 // A value transformer for mapping "url" to "3" and vice versa in the
156 // "textMode" preference, since NSMatrix uses NSInteger selectedIndex.
159 @interface TextModeTransformer: NSValueTransformer {}
161 @implementation TextModeTransformer
162 + (Class)transformedValueClass { return [NSString class]; }
163 + (BOOL)allowsReverseTransformation { return YES; }
165 - (id)transformedValue:(id)value {
166 if ([value isKindOfClass:[NSString class]]) {
168 if ([value isEqualToString:@"date"]) { i = 0; }
169 else if ([value isEqualToString:@"literal"]) { i = 1; }
170 else if ([value isEqualToString:@"file"]) { i = 2; }
171 else if ([value isEqualToString:@"url"]) { i = 3; }
172 else if ([value isEqualToString:@"program"]) { i = 4; }
174 value = [NSNumber numberWithInt: i];
179 - (id)reverseTransformedValue:(id)value {
180 if ([value isKindOfClass:[NSNumber class]]) {
181 switch ((int) [value doubleValue]) {
182 case 0: value = @"date"; break;
183 case 1: value = @"literal"; break;
184 case 2: value = @"file"; break;
185 case 3: value = @"url"; break;
186 case 4: value = @"program"; break;
195 #pragma mark Implementing radio buttons
197 /* The UIPickerView is a hideous and uncustomizable piece of shit.
198 I can't believe Apple actually released that thing on the world.
199 Let's fake up some radio buttons instead.
202 #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW)
204 @interface RadioButton : UILabel
210 @property(nonatomic) int index;
211 @property(nonatomic, retain) NSArray *items;
215 @implementation RadioButton
220 - (id) initWithIndex:(int)_index items:_items
222 self = [super initWithFrame:CGRectZero];
224 items = [_items retain];
226 [self setText: [[items objectAtIndex:index] objectAtIndex:0]];
227 [self setBackgroundColor:[UIColor clearColor]];
236 # endif // !USE_PICKER_VIEW
239 # pragma mark Implementing labels with clickable links
241 #if defined(USE_IPHONE) && defined(USE_HTML_LABELS)
243 @interface HTMLLabel : UIView <UIWebViewDelegate>
250 @property(nonatomic, retain) NSString *html;
251 @property(nonatomic, retain) UIWebView *webView;
253 - (id) initWithHTML:(NSString *)h font:(UIFont *)f;
254 - (id) initWithText:(NSString *)t font:(UIFont *)f;
255 - (void) setHTML:(NSString *)h;
256 - (void) setText:(NSString *)t;
261 @implementation HTMLLabel
266 - (id) initWithHTML:(NSString *)h font:(UIFont *)f
269 if (! self) return 0;
271 webView = [[UIWebView alloc] init];
272 webView.delegate = self;
273 webView.dataDetectorTypes = UIDataDetectorTypeNone;
274 self. autoresizingMask = UIViewAutoresizingNone; // we do it manually
275 webView.autoresizingMask = UIViewAutoresizingNone;
276 webView.scrollView.scrollEnabled = NO;
277 webView.scrollView.bounces = NO;
279 [webView setBackgroundColor:[UIColor clearColor]];
281 [self addSubview: webView];
286 - (id) initWithText:(NSString *)t font:(UIFont *)f
288 self = [self initWithHTML:@"" font:f];
289 if (! self) return 0;
295 - (void) setHTML: (NSString *)h
299 if (html) [html release];
302 [NSString stringWithFormat:
303 @"<!DOCTYPE HTML PUBLIC "
304 "\"-//W3C//DTD HTML 4.01 Transitional//EN\""
305 " \"http://www.w3.org/TR/html4/loose.dtd\">"
308 // "<META NAME=\"viewport\" CONTENT=\""
309 // "width=device-width"
310 // "initial-scale=1.0;"
311 // "maximum-scale=1.0;\">"
315 " margin: 0; padding: 0; border: 0;"
316 " font-family: \"%@\";"
317 " font-size: %.4fpx;" // Must be "px", not "pt"!
318 " line-height: %.4fpx;" // And no spaces before it.
319 " -webkit-text-size-adjust: none;"
332 [webView stopLoading];
333 [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
337 static char *anchorize (const char *url);
339 - (void) setText: (NSString *)t
341 t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
342 whitespaceCharacterSet]];
343 t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
344 t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
345 t = [t stringByReplacingOccurrencesOfString:@">" withString:@">"];
346 t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" <P> "];
347 t = [t stringByReplacingOccurrencesOfString:@"<P> "
348 withString:@"<P> "];
349 t = [t stringByReplacingOccurrencesOfString:@"\n "
350 withString:@"<BR> "];
354 [t componentsSeparatedByCharactersInSet:
355 [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
356 if ([s hasPrefix:@"http://"] ||
357 [s hasPrefix:@"https://"]) {
358 char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
359 NSString *a2 = [NSString stringWithCString: anchor
360 encoding: NSUTF8StringEncoding];
361 s = [NSString stringWithFormat: @"<A HREF=\"%@\">%@</A><BR>", s, a2];
364 h = [NSString stringWithFormat: @"%@ %@", h, s];
367 h = [h stringByReplacingOccurrencesOfString:@" <P> " withString:@"<P>"];
368 h = [h stringByReplacingOccurrencesOfString:@"<BR><P>" withString:@"<P>"];
369 h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
370 whitespaceAndNewlineCharacterSet]];
376 -(BOOL) webView:(UIWebView *)wv
377 shouldStartLoadWithRequest:(NSURLRequest *)req
378 navigationType:(UIWebViewNavigationType)type
380 // Force clicked links to open in Safari, not in this window.
381 if (type == UIWebViewNavigationTypeLinkClicked) {
382 [[UIApplication sharedApplication] openURL:[req URL]];
389 - (void) setFrame: (CGRect)r
394 [webView setFrame: r];
398 - (NSString *) stripTags:(NSString *)str
400 NSString *result = @"";
403 str = [str stringByReplacingOccurrencesOfString:@"<P>"
404 withString:@"<BR><BR>"
405 options:NSCaseInsensitiveSearch
406 range:NSMakeRange(0, [str length])];
407 str = [str stringByReplacingOccurrencesOfString:@"<BR>"
409 options:NSCaseInsensitiveSearch
410 range:NSMakeRange(0, [str length])];
413 for (NSString *s in [str componentsSeparatedByString: @"<"]) {
414 NSRange r = [s rangeOfString:@">"];
416 s = [s substringFromIndex: r.location + r.length];
417 result = [result stringByAppendingString: s];
420 // Compress internal horizontal whitespace.
423 for (NSString *s in [str componentsSeparatedByCharactersInSet:
424 [NSCharacterSet whitespaceCharacterSet]]) {
425 if ([result length] == 0)
427 else if ([s length] > 0)
428 result = [NSString stringWithFormat: @"%@ %@", result, s];
437 CGRect r = [self frame];
439 /* It would be sensible to just ask the UIWebView how tall the page is,
440 instead of hoping that NSString and UIWebView measure fonts and do
441 wrapping in exactly the same way, but since UIWebView is asynchronous,
442 we'd have to wait for the document to load first, e.g.:
444 - Start the document loading;
445 - return a default height to use for the UITableViewCell;
446 - wait for the webViewDidFinishLoad delegate method to fire;
447 - then force the UITableView to reload, to pick up the new height.
449 But I couldn't make that work.
452 r.size.height = [[webView
453 stringByEvaluatingJavaScriptFromString:
454 @"document.body.offsetHeight"]
457 NSString *text = [self stripTags: html];
460 s = [text boundingRectWithSize:s
461 options:NSStringDrawingUsesLineFragmentOrigin
462 attributes:@{NSFontAttributeName: font}
464 r.size.height = s.height;
481 #endif // USE_IPHONE && USE_HTML_LABELS
484 @interface XScreenSaverConfigSheet (Private)
486 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
489 - (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
490 - (void) placeChild: (NSView *)c on:(NSView *)p;
491 static NSView *last_child (NSView *parent);
492 static void layout_group (NSView *group, BOOL horiz_p);
494 - (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
495 - (void) placeChild: (NSObject *)c on:(NSView *)p;
496 - (void) placeSeparator;
497 - (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
498 - (void) refreshTableView;
499 # endif // USE_IPHONE
504 @implementation XScreenSaverConfigSheet
506 # define LEFT_MARGIN 20 // left edge of window
507 # define COLUMN_SPACING 10 // gap between e.g. labels and text fields
508 # define LEFT_LABEL_WIDTH 70 // width of all left labels
509 # define LINE_SPACING 10 // leading between each line
511 # define FONT_SIZE 17 // Magic hardcoded UITableView font size.
513 #pragma mark Talking to the resource database
516 /* Normally we read resources by looking up "KEY" in the database
517 "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
518 app, everything is stored in the database "org.jwz.xscreensaver"
519 instead, so transform keys to "SAVERNAME.KEY".
521 NOTE: This is duplicated in PrefsReader.m, cause I suck.
523 - (NSString *) makeKey:(NSString *)key
526 NSString *prefix = [saver_name stringByAppendingString:@"."];
527 if (! [key hasPrefix:prefix]) // Don't double up!
528 key = [prefix stringByAppendingString:key];
534 - (NSString *) makeCKey:(const char *)key
536 return [self makeKey:[NSString stringWithCString:key
537 encoding:NSUTF8StringEncoding]];
541 /* Given a command-line option, returns the corresponding resource name.
542 Any arguments in the switch string are ignored (e.g., "-foo x").
544 - (NSString *) switchToResource:(NSString *)cmdline_switch
545 opts:(const XrmOptionDescRec *)opts_array
546 valRet:(NSString **)val_ret
550 NSAssert(cmdline_switch, @"cmdline switch is null");
551 if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
552 encoding:NSUTF8StringEncoding]) {
553 NSAssert1(0, @"unable to convert %@", cmdline_switch);
556 char *s = strpbrk(buf, " \t\r\n");
560 while (*tail && (*tail == ' ' || *tail == '\t'))
564 while (opts_array[0].option) {
565 if (!strcmp (opts_array[0].option, buf)) {
568 if (opts_array[0].argKind == XrmoptionNoArg) {
570 NSAssert1 (0, @"expected no args to switch: \"%@\"",
572 ret = opts_array[0].value;
575 NSAssert1 (0, @"expected args to switch: \"%@\"",
582 ? [NSString stringWithCString:ret
583 encoding:NSUTF8StringEncoding]
586 const char *res = opts_array[0].specifier;
587 while (*res && (*res == '.' || *res == '*'))
589 return [self makeCKey:res];
594 NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
599 - (NSUserDefaultsController *)controllerForKey:(NSString *)key
601 static NSDictionary *a = 0;
603 a = UPDATER_DEFAULTS;
606 if ([a objectForKey:key])
607 // These preferences are global to all xscreensavers.
608 return globalDefaultsController;
610 // All other preferences are per-saver.
611 return userDefaultsController;
617 // Called when a slider is bonked.
619 - (void)sliderAction:(UISlider*)sender
621 if ([active_text_field canResignFirstResponder])
622 [active_text_field resignFirstResponder];
623 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
625 // Hacky API. See comment in InvertedSlider.m.
626 double v = ([sender isKindOfClass: [InvertedSlider class]]
627 ? [(InvertedSlider *) sender transformedValue]
630 [[self controllerForKey:pref_key]
631 setObject:((v == (int) v)
632 ? [NSNumber numberWithInt:(int) v]
633 : [NSNumber numberWithDouble: v])
637 // Called when a checkbox/switch is bonked.
639 - (void)switchAction:(UISwitch*)sender
641 if ([active_text_field canResignFirstResponder])
642 [active_text_field resignFirstResponder];
643 NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
644 NSString *v = ([sender isOn] ? @"true" : @"false");
645 [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
648 # ifdef USE_PICKER_VIEW
649 // Called when a picker is bonked.
651 - (void)pickerView:(UIPickerView *)pv
652 didSelectRow:(NSInteger)row
653 inComponent:(NSInteger)column
655 if ([active_text_field canResignFirstResponder])
656 [active_text_field resignFirstResponder];
658 NSAssert (column == 0, @"internal error");
659 NSArray *a = [picker_values objectAtIndex: [pv tag]];
660 if (! a) return; // Too early?
661 a = [a objectAtIndex:row];
662 NSAssert (a, @"missing row");
664 //NSString *label = [a objectAtIndex:0];
665 NSString *pref_key = [a objectAtIndex:1];
666 NSObject *pref_val = [a objectAtIndex:2];
667 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
669 # else // !USE_PICKER_VIEW
671 // Called when a RadioButton is bonked.
673 - (void)radioAction:(RadioButton*)sender
675 if ([active_text_field canResignFirstResponder])
676 [active_text_field resignFirstResponder];
678 NSArray *item = [[sender items] objectAtIndex: [sender index]];
679 NSString *pref_key = [item objectAtIndex:1];
680 NSObject *pref_val = [item objectAtIndex:2];
681 [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
684 - (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
686 active_text_field = tf;
690 - (void)textFieldDidEndEditing:(UITextField *)tf
692 NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
693 NSString *txt = [tf text];
694 [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
697 - (BOOL)textFieldShouldReturn:(UITextField *)tf
699 active_text_field = nil;
700 [tf resignFirstResponder];
704 # endif // !USE_PICKER_VIEW
711 - (void) okAction:(NSObject *)arg
713 // Without the setAppliesImmediately:, when the saver restarts, it's still
714 // got the old settings. -[XScreenSaverConfigSheet traverseTree] sets this
715 // to NO; default is YES.
717 // #### However: I'm told that when these are set to YES, then changes to
718 // 'textLiteral', 'textURL' and 'textProgram' are ignored, but 'textFile'
719 // works. In StarWars, at least...
721 [userDefaultsController setAppliesImmediately:YES];
722 [globalDefaultsController setAppliesImmediately:YES];
723 [userDefaultsController commitEditing];
724 [globalDefaultsController commitEditing];
725 [userDefaultsController save:self];
726 [globalDefaultsController save:self];
727 [NSApp endSheet:self returnCode:NSOKButton];
731 - (void) cancelAction:(NSObject *)arg
733 [userDefaultsController revert:self];
734 [globalDefaultsController revert:self];
735 [NSApp endSheet:self returnCode:NSCancelButton];
738 # endif // !USE_IPHONE
741 - (void) resetAction:(NSObject *)arg
744 [userDefaultsController revertToInitialValues:self];
745 [globalDefaultsController revertToInitialValues:self];
748 for (NSString *key in defaultOptions) {
749 NSObject *val = [defaultOptions objectForKey:key];
750 [[self controllerForKey:key] setObject:val forKey:key];
753 for (UIControl *ctl in pref_ctls) {
754 NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
755 [self bindResource:ctl key:pref_key reload:YES];
758 [self refreshTableView];
759 # endif // USE_IPHONE
763 /* Connects a control (checkbox, etc) to the corresponding preferences key.
765 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
766 reload:(BOOL)reload_p
768 NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
770 NSDictionary *opts_dict = nil;
771 NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
773 : ([control isKindOfClass:[NSMatrix class]]
777 if ([control isKindOfClass:[NSMatrix class]]) {
778 opts_dict = @{ NSValueTransformerNameBindingOption:
779 @"TextModeTransformer" };
784 withKeyPath:[@"values." stringByAppendingString: pref_key]
789 NSObject *val = [prefs objectForKey:pref_key];
793 if ([val isKindOfClass:[NSString class]]) {
794 sval = (NSString *) val;
795 if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
796 NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
797 NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
800 dval = [sval doubleValue];
801 } else if ([val isKindOfClass:[NSNumber class]]) {
802 // NSBoolean (__NSCFBoolean) is really NSNumber.
803 dval = [(NSNumber *) val doubleValue];
804 sval = [(NSNumber *) val stringValue];
807 if ([control isKindOfClass:[UISlider class]]) {
808 sel = @selector(sliderAction:);
809 // Hacky API. See comment in InvertedSlider.m.
810 if ([control isKindOfClass:[InvertedSlider class]])
811 [(InvertedSlider *) control setTransformedValue: dval];
813 [(UISlider *) control setValue: dval];
814 } else if ([control isKindOfClass:[UISwitch class]]) {
815 sel = @selector(switchAction:);
816 [(UISwitch *) control setOn: ((int) dval != 0)];
817 # ifdef USE_PICKER_VIEW
818 } else if ([control isKindOfClass:[UIPickerView class]]) {
820 [(UIPickerView *) control selectRow:((int)dval) inComponent:0
822 # else // !USE_PICKER_VIEW
823 } else if ([control isKindOfClass:[RadioButton class]]) {
824 sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
825 } else if ([control isKindOfClass:[UITextField class]]) {
827 [(UITextField *) control setText: sval];
828 # endif // !USE_PICKER_VIEW
830 NSAssert (0, @"unknown class");
833 // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
837 pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
838 pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
841 [pref_keys addObject: [self makeKey:pref_key]];
842 [pref_ctls addObject: control];
843 ((UIControl *) control).tag = [pref_keys count] - 1;
846 [(UIControl *) control addTarget:self action:sel
847 forControlEvents:UIControlEventValueChanged];
851 # endif // USE_IPHONE
854 NSObject *def = [[prefs defaults] objectForKey:pref_key];
855 NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
856 s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
857 s = [NSString stringWithFormat:@"%@ = %@", s,
858 ([def isKindOfClass:[NSString class]]
859 ? [NSString stringWithFormat:@"\"%@\"", def]
861 s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0];
862 s = [NSString stringWithFormat:@"%@ %@ / %@", s,
863 [def class], [control class]];
865 s = [NSString stringWithFormat:@"%@ / %@", s, bindto];
872 - (void) bindResource:(NSObject *)control key:(NSString *)pref_key
874 [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
879 - (void) bindSwitch:(NSObject *)control
880 cmdline:(NSString *)cmd
882 [self bindResource:control
883 key:[self switchToResource:cmd opts:opts valRet:0]];
887 #pragma mark Text-manipulating utilities
891 unwrap (NSString *text)
893 // Unwrap lines: delete \n but do not delete \n\n.
895 NSArray *lines = [text componentsSeparatedByString:@"\n"];
896 NSUInteger i, nlines = [lines count];
899 text = @"\n"; // start with one blank line
901 // skip trailing blank lines in file
902 for (i = nlines-1; i > 0; i--) {
903 NSString *s = (NSString *) [lines objectAtIndex:i];
909 // skip leading blank lines in file
910 for (i = 0; i < nlines; i++) {
911 NSString *s = (NSString *) [lines objectAtIndex:i];
918 for (; i < nlines; i++) {
919 NSString *s = (NSString *) [lines objectAtIndex:i];
920 if ([s length] == 0) {
921 text = [text stringByAppendingString:@"\n\n"];
923 } else if ([s characterAtIndex:0] == ' ' ||
924 [s hasPrefix:@"Copyright "] ||
925 [s hasPrefix:@"https://"] ||
926 [s hasPrefix:@"http://"]) {
927 // don't unwrap if the following line begins with whitespace,
928 // or with the word "Copyright", or if it begins with a URL.
930 text = [text stringByAppendingString:@"\n"];
931 text = [text stringByAppendingString:s];
936 text = [text stringByAppendingString:@" "];
937 text = [text stringByAppendingString:s];
948 /* Makes the text up to the first comma be bold.
951 boldify (NSText *nstext)
953 NSString *text = [nstext string];
954 NSRange r = [text rangeOfString:@"," options:0];
955 r.length = r.location+1;
959 NSFont *font = [nstext font];
960 font = [NSFont boldSystemFontOfSize:[font pointSize]];
961 [nstext setFont:font range:r];
963 # endif // !USE_IPHONE
966 /* Creates a human-readable anchor to put on a URL.
969 anchorize (const char *url)
971 const char *wiki1 = "http://en.wikipedia.org/wiki/";
972 const char *wiki2 = "https://en.wikipedia.org/wiki/";
973 const char *math1 = "http://mathworld.wolfram.com/";
974 const char *math2 = "https://mathworld.wolfram.com/";
975 if (!strncmp (wiki1, url, strlen(wiki1)) ||
976 !strncmp (wiki2, url, strlen(wiki2))) {
977 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
978 strcpy (anchor, "Wikipedia: \"");
979 const char *in = url + strlen(!strncmp (wiki1, url, strlen(wiki1))
981 char *out = anchor + strlen(anchor);
985 } else if (*in == '#') {
988 } else if (*in == '%') {
994 sscanf (hex, "%x", &n);
1006 } else if (!strncmp (math1, url, strlen(math1)) ||
1007 !strncmp (math2, url, strlen(math2))) {
1008 char *anchor = (char *) malloc (strlen(url) * 3 + 10);
1009 strcpy (anchor, "MathWorld: \"");
1010 const char *start = url + strlen(!strncmp (math1, url, strlen(math1))
1012 const char *in = start;
1013 char *out = anchor + strlen(anchor);
1017 } else if (in != start && *in >= 'A' && *in <= 'Z') {
1020 } else if (!strncmp (in, ".htm", 4)) {
1032 return strdup (url);
1037 #if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
1039 /* Converts any http: URLs in the given text field to clickable links.
1042 hreffify (NSText *nstext)
1045 NSString *text = [nstext string];
1046 [nstext setRichText:YES];
1048 NSString *text = [nstext text];
1051 NSUInteger L = [text length];
1052 NSRange start; // range is start-of-search to end-of-string
1055 while (start.location < L) {
1057 // Find the beginning of a URL...
1059 NSRange r2 = [text rangeOfString: @"http://" options:0 range:start];
1060 NSRange r3 = [text rangeOfString:@"https://" options:0 range:start];
1061 if ((r2.location == NSNotFound &&
1062 r3.location != NSNotFound) ||
1063 (r2.location != NSNotFound &&
1064 r3.location != NSNotFound &&
1065 r3.location < r2.location))
1067 if (r2.location == NSNotFound)
1070 // Next time around, start searching after this.
1071 start.location = r2.location + r2.length;
1072 start.length = L - start.location;
1074 // Find the end of a URL (whitespace or EOF)...
1076 r3 = [text rangeOfCharacterFromSet:
1077 [NSCharacterSet whitespaceAndNewlineCharacterSet]
1078 options:0 range:start];
1079 if (r3.location == NSNotFound) // EOF
1080 r3.location = L, r3.length = 0;
1082 // Next time around, start searching after this.
1083 start.location = r3.location;
1084 start.length = L - start.location;
1086 // Set r2 to the start/length of this URL.
1087 r2.length = start.location - r2.location;
1090 NSString *nsurl = [text substringWithRange:r2];
1091 const char *url = [nsurl UTF8String];
1093 // If this is a Wikipedia URL, make the linked text be prettier.
1095 char *anchor = anchorize(url);
1099 // Construct the RTF corresponding to <A HREF="url">anchor</A>
1101 const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
1102 char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
1103 sprintf (rtf, fmt, url, anchor);
1105 NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
1106 [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
1108 # else // !USE_IPHONE
1109 // *anchor = 0; // Omit Wikipedia anchor
1110 text = [text stringByReplacingCharactersInRange:r2
1111 withString:[NSString stringWithCString:anchor
1112 encoding:NSUTF8StringEncoding]];
1113 // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
1114 // withString:@"\n\n"];
1115 # endif // !USE_IPHONE
1119 NSUInteger L2 = [text length]; // might have changed
1120 start.location -= (L - L2);
1125 [nstext setText:text];
1130 #endif /* !USE_IPHONE || !USE_HTML_LABELS */
1134 #pragma mark Creating controls from XML
1137 /* Parse the attributes of an XML tag into a dictionary.
1138 For input, the dictionary should have as attributes the keys, each
1139 with @"" as their value.
1140 On output, the dictionary will set the keys to the values specified,
1141 and keys that were not specified will not be present in the dictionary.
1142 Warnings are printed if there are duplicate or unknown attributes.
1144 - (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
1146 NSArray *attrs = [(NSXMLElement *) node attributes];
1147 NSUInteger n = [attrs count];
1150 // For each key in the dictionary, fill in the dict with the corresponding
1151 // value. The value @"" is assumed to mean "un-set". Issue a warning if
1152 // an attribute is specified twice.
1154 for (i = 0; i < n; i++) {
1155 NSXMLNode *attr = [attrs objectAtIndex:i];
1156 NSString *key = [attr name];
1157 NSString *val = [attr objectValue];
1158 NSString *old = [dict objectForKey:key];
1161 NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
1162 } else if ([old length] != 0) {
1163 NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
1165 [dict setValue:val forKey:key];
1169 // Remove from the dictionary any keys whose value is still @"",
1170 // meaning there was no such attribute specified.
1172 NSArray *keys = [dict allKeys];
1174 for (i = 0; i < n; i++) {
1175 NSString *key = [keys objectAtIndex:i];
1176 NSString *val = [dict objectForKey:key];
1177 if ([val length] == 0)
1178 [dict removeObjectForKey:key];
1182 // Kludge for starwars.xml:
1183 // If there is a "_low-label" and no "_label", but "_low-label" contains
1184 // spaces, divide them.
1185 NSString *lab = [dict objectForKey:@"_label"];
1186 NSString *low = [dict objectForKey:@"_low-label"];
1189 [[[low stringByTrimmingCharactersInSet:
1190 [NSCharacterSet whitespaceAndNewlineCharacterSet]]
1191 componentsSeparatedByString: @" "]
1192 filteredArrayUsingPredicate:
1193 [NSPredicate predicateWithFormat:@"length > 0"]];
1194 if (split && [split count] == 2) {
1195 [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
1196 [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
1199 # endif // USE_IPHONE
1203 /* Handle the options on the top level <xscreensaver> tag.
1205 - (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
1207 NSMutableDictionary *dict = [@{ @"name": @"",
1211 [self parseAttrs:dict node:node];
1212 NSString *name = [dict objectForKey:@"name"];
1213 NSString *label = [dict objectForKey:@"_label"];
1217 NSAssert1 (label, @"no _label in %@", [node name]);
1218 NSAssert1 (name, @"no name in \"%@\"", label);
1223 /* Creates a label: an un-editable NSTextField displaying the given text.
1225 - (LABEL *) makeLabel:(NSString *)text
1228 rect.origin.x = rect.origin.y = 0;
1229 rect.size.width = rect.size.height = 10;
1231 NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
1232 [lab setSelectable:NO];
1233 [lab setEditable:NO];
1234 [lab setBezeled:NO];
1235 [lab setDrawsBackground:NO];
1236 [lab setStringValue:text];
1238 # else // USE_IPHONE
1239 UILabel *lab = [[UILabel alloc] initWithFrame:rect];
1240 [lab setText: [text stringByTrimmingCharactersInSet:
1241 [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
1242 [lab setBackgroundColor:[UIColor clearColor]];
1243 [lab setNumberOfLines:0]; // unlimited
1244 // [lab setLineBreakMode:UILineBreakModeWordWrap];
1245 [lab setLineBreakMode:NSLineBreakByTruncatingHead];
1246 [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
1247 UIViewAutoresizingFlexibleHeight)];
1248 # endif // USE_IPHONE
1254 /* Creates the checkbox (NSButton) described by the given XML node.
1256 - (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
1258 NSMutableDictionary *dict = [@{ @"id": @"",
1263 [self parseAttrs:dict node:node];
1264 NSString *label = [dict objectForKey:@"_label"];
1265 NSString *arg_set = [dict objectForKey:@"arg-set"];
1266 NSString *arg_unset = [dict objectForKey:@"arg-unset"];
1271 NSAssert1 (0, @"no _label in %@", [node name]);
1274 if (!arg_set && !arg_unset) {
1275 NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
1278 if (arg_set && arg_unset) {
1279 NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
1283 // sanity-check the choice of argument names.
1285 if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
1286 [arg_set hasPrefix:@"--no-"]))
1287 NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
1289 if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
1290 ![arg_unset hasPrefix:@"--no-"]))
1291 NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
1295 rect.origin.x = rect.origin.y = 0;
1296 rect.size.width = rect.size.height = 10;
1300 NSButton *button = [[NSButton alloc] initWithFrame:rect];
1301 [button setButtonType:NSSwitchButton];
1302 [button setTitle:label];
1304 [self placeChild:button on:parent];
1306 # else // USE_IPHONE
1308 LABEL *lab = [self makeLabel:label];
1309 [self placeChild:lab on:parent];
1310 UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
1311 [self placeChild:button on:parent right:YES];
1313 # endif // USE_IPHONE
1315 [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
1320 /* Creates the number selection control described by the given XML node.
1321 If "type=slider", it's an NSSlider.
1322 If "type=spinbutton", it's a text field with up/down arrows next to it.
1324 - (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
1326 NSMutableDictionary *dict = [@{ @"id": @"",
1329 @"_high-label": @"",
1337 [self parseAttrs:dict node:node];
1338 NSString *label = [dict objectForKey:@"_label"];
1339 NSString *low_label = [dict objectForKey:@"_low-label"];
1340 NSString *high_label = [dict objectForKey:@"_high-label"];
1341 NSString *type = [dict objectForKey:@"type"];
1342 NSString *arg = [dict objectForKey:@"arg"];
1343 NSString *low = [dict objectForKey:@"low"];
1344 NSString *high = [dict objectForKey:@"high"];
1345 NSString *def = [dict objectForKey:@"default"];
1346 NSString *cvt = [dict objectForKey:@"convert"];
1350 NSAssert1 (arg, @"no arg in %@", label);
1351 NSAssert1 (type, @"no type in %@", label);
1354 NSAssert1 (0, @"no low in %@", [node name]);
1358 NSAssert1 (0, @"no high in %@", [node name]);
1362 NSAssert1 (0, @"no default in %@", [node name]);
1365 if (cvt && ![cvt isEqualToString:@"invert"]) {
1366 NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
1370 // If either the min or max field contains a decimal point, then this
1371 // option may have a floating point value; otherwise, it is constrained
1372 // to be an integer.
1374 NSCharacterSet *dot =
1375 [NSCharacterSet characterSetWithCharactersInString:@"."];
1376 BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
1377 [high rangeOfCharacterFromSet:dot].location != NSNotFound);
1379 if ([type isEqualToString:@"slider"]
1380 # ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1381 || [type isEqualToString:@"spinbutton"]
1386 rect.origin.x = rect.origin.y = 0;
1387 rect.size.width = 150;
1388 rect.size.height = 23; // apparent min height for slider with ticks...
1390 slider = [[InvertedSlider alloc] initWithFrame:rect
1392 integers: !float_p];
1393 [slider setMaxValue:[high doubleValue]];
1394 [slider setMinValue:[low doubleValue]];
1396 int range = [slider maxValue] - [slider minValue] + 1;
1399 while (range2 > max_ticks)
1403 // If we have elided ticks, leave it at the max number of ticks.
1404 if (range != range2 && range2 < max_ticks)
1407 // If it's a float, always display the max number of ticks.
1408 if (float_p && range2 < max_ticks)
1411 [slider setNumberOfTickMarks:range2];
1413 [slider setAllowsTickMarkValuesOnly:
1414 (range == range2 && // we are showing the actual number of ticks
1415 !float_p)]; // and we want integer results
1416 # endif // !USE_IPHONE
1418 // #### Note: when the slider's range is large enough that we aren't
1419 // showing all possible ticks, the slider's value is not constrained
1420 // to be an integer, even though it should be...
1421 // Maybe we need to use a value converter or something?
1425 lab = [self makeLabel:label];
1426 [self placeChild:lab on:parent];
1429 CGFloat s = [NSFont systemFontSize] + 4;
1430 [lab setFont:[NSFont boldSystemFontOfSize:s]];
1436 lab = [self makeLabel:low_label];
1437 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1439 [lab setAlignment:1]; // right aligned
1441 if (rect.size.width < LEFT_LABEL_WIDTH)
1442 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1443 rect.size.height = [slider frame].size.height;
1444 [lab setFrame:rect];
1445 [self placeChild:lab on:parent];
1446 # else // USE_IPHONE
1447 [lab setTextAlignment: NSTextAlignmentRight];
1448 // Sometimes rotation screws up truncation.
1449 [lab setLineBreakMode:NSLineBreakByClipping];
1450 [self placeChild:lab on:parent right:(label ? YES : NO)];
1451 # endif // USE_IPHONE
1455 [self placeChild:slider on:parent right:(low_label ? YES : NO)];
1456 # else // USE_IPHONE
1457 [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
1458 # endif // USE_IPHONE
1461 // Make left label be same height as slider.
1463 rect.size.height = [slider frame].size.height;
1464 [lab setFrame:rect];
1468 rect = [slider frame];
1469 if (rect.origin.x < LEFT_LABEL_WIDTH)
1470 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
1471 [slider setFrame:rect];
1475 lab = [self makeLabel:high_label];
1476 [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1479 // Make right label be same height as slider.
1480 rect.size.height = [slider frame].size.height;
1481 [lab setFrame:rect];
1483 // Sometimes rotation screws up truncation.
1484 [lab setLineBreakMode:NSLineBreakByClipping];
1486 [self placeChild:lab on:parent right:YES];
1489 [self bindSwitch:slider cmdline:arg];
1492 #ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
1494 } else if ([type isEqualToString:@"spinbutton"]) {
1497 NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
1500 NSAssert1 (!low_label,
1501 @"low-label not allowed in spinbutton \"%@\"", [node name]);
1502 NSAssert1 (!high_label,
1503 @"high-label not allowed in spinbutton \"%@\"", [node name]);
1504 NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
1508 rect.origin.x = rect.origin.y = 0;
1509 rect.size.width = rect.size.height = 10;
1511 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1512 [txt setStringValue:@"0000.0"];
1514 [txt setStringValue:@""];
1517 LABEL *lab = [self makeLabel:label];
1518 //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
1519 [lab setAlignment:1]; // right aligned
1521 if (rect.size.width < LEFT_LABEL_WIDTH)
1522 rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
1523 rect.size.height = [txt frame].size.height;
1524 [lab setFrame:rect];
1525 [self placeChild:lab on:parent];
1528 [self placeChild:txt on:parent right:(label ? YES : NO)];
1532 if (rect.origin.x < LEFT_LABEL_WIDTH)
1533 rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
1534 [txt setFrame:rect];
1537 rect.size.width = rect.size.height = 10;
1538 NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
1540 [self placeChild:step on:parent right:YES];
1541 rect = [step frame];
1542 rect.origin.x -= COLUMN_SPACING; // this one goes close
1543 rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
1544 [step setFrame:rect];
1546 [step setMinValue:[low doubleValue]];
1547 [step setMaxValue:[high doubleValue]];
1548 [step setAutorepeat:YES];
1549 [step setValueWraps:NO];
1551 double range = [high doubleValue] - [low doubleValue];
1553 [step setIncrement:range / 10.0];
1554 else if (range >= 500)
1555 [step setIncrement:range / 100.0];
1557 [step setIncrement:1.0];
1559 NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
1560 [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
1561 [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
1562 [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
1563 [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
1564 [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
1565 [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
1567 [fmt setGeneratesDecimalNumbers:float_p];
1568 [[txt cell] setFormatter:fmt];
1570 [self bindSwitch:step cmdline:arg];
1571 [self bindSwitch:txt cmdline:arg];
1576 # endif // USE_IPHONE
1579 NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
1586 set_menu_item_object (NSMenuItem *item, NSObject *obj)
1588 /* If the object associated with this menu item looks like a boolean,
1589 store an NSNumber instead of an NSString, since that's what
1590 will be in the preferences (due to similar logic in PrefsReader).
1592 if ([obj isKindOfClass:[NSString class]]) {
1593 NSString *string = (NSString *) obj;
1594 if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
1595 NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
1596 obj = [NSNumber numberWithBool:YES];
1597 else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
1598 NSOrderedSame == [string caseInsensitiveCompare:@"no"])
1599 obj = [NSNumber numberWithBool:NO];
1604 [item setRepresentedObject:obj];
1605 //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
1607 # endif // !USE_IPHONE
1610 /* Creates the popup menu described by the given XML node (and its children).
1612 - (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
1614 NSArray *children = [node children];
1615 NSUInteger i, count = [children count];
1618 NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
1622 // get the "id" attribute off the <select> tag.
1624 NSMutableDictionary *dict = [@{ @"id": @"", } mutableCopy];
1625 [self parseAttrs:dict node:node];
1630 rect.origin.x = rect.origin.y = 0;
1631 rect.size.width = 10;
1632 rect.size.height = 10;
1634 NSString *menu_key = nil; // the resource key used by items in this menu
1637 // #### "Build and Analyze" says that all of our widgets leak, because it
1638 // seems to not realize that placeChild -> addSubview retains them.
1639 // Not sure what to do to make these warnings go away.
1641 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1643 NSMenuItem *def_item = nil;
1644 float max_width = 0;
1646 # else // USE_IPHONE
1648 NSString *def_item = nil;
1650 rect.size.width = 0;
1651 rect.size.height = 0;
1652 # ifdef USE_PICKER_VIEW
1653 UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain];
1654 popup.delegate = self;
1655 popup.dataSource = self;
1656 # endif // !USE_PICKER_VIEW
1657 NSMutableArray *items = [NSMutableArray arrayWithCapacity:10];
1659 # endif // USE_IPHONE
1661 for (i = 0; i < count; i++) {
1662 NSXMLNode *child = [children objectAtIndex:i];
1664 if ([child kind] == NSXMLCommentKind)
1666 if ([child kind] != NSXMLElementKind) {
1667 // NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node);
1671 // get the "id", "_label", and "arg-set" attrs off of the <option> tags.
1673 NSMutableDictionary *dict2 = [@{ @"id": @"",
1677 [self parseAttrs:dict2 node:child];
1678 NSString *label = [dict2 objectForKey:@"_label"];
1679 NSString *arg_set = [dict2 objectForKey:@"arg-set"];
1684 NSAssert1 (0, @"no _label in %@", [child name]);
1689 // create the menu item (and then get a pointer to it)
1690 [popup addItemWithTitle:label];
1691 NSMenuItem *item = [popup itemWithTitle:label];
1692 # endif // USE_IPHONE
1695 NSString *this_val = NULL;
1696 NSString *this_key = [self switchToResource: arg_set
1699 NSAssert1 (this_val, @"this_val null for %@", arg_set);
1700 if (menu_key && ![menu_key isEqualToString:this_key])
1702 @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"",
1703 menu_key, this_key, this_val);
1705 menu_key = this_key;
1707 /* If this menu has the cmd line "-mode foo" then set this item's
1708 value to "foo" (the menu itself will be bound to e.g. "modeString")
1711 set_menu_item_object (item, this_val);
1713 // Array holds ["Label", "resource-key", "resource-val"].
1714 [items addObject:[NSMutableArray arrayWithObjects:
1715 label, @"", this_val, nil]];
1719 // no arg-set -- only one menu item can be missing that.
1720 NSAssert1 (!def_item, @"no arg-set in \"%@\"", label);
1725 // Array holds ["Label", "resource-key", "resource-val"].
1726 [items addObject:[NSMutableArray arrayWithObjects:
1727 label, @"", @"", nil]];
1731 /* make sure the menu button has room for the text of this item,
1732 and remember the greatest width it has reached.
1735 [popup setTitle:label];
1737 NSRect r = [popup frame];
1738 if (r.size.width > max_width) max_width = r.size.width;
1739 # endif // USE_IPHONE
1743 NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]);
1747 /* We've added all of the menu items. If there was an item with no
1748 command-line switch, then it's the item that represents the default
1749 value. Now we must bind to that item as well... (We have to bind
1750 this one late, because if it was the first item, then we didn't
1751 yet know what resource was associated with this menu.)
1754 NSObject *def_obj = [defaultOptions objectForKey:menu_key];
1756 @"no default value for resource \"%@\" in menu item \"%@\"",
1766 set_menu_item_object (def_item, def_obj);
1767 # else // !USE_IPHONE
1768 for (NSMutableArray *a in items) {
1769 // Make sure each array contains the resource key.
1770 [a replaceObjectAtIndex:1 withObject:menu_key];
1771 // Make sure the default item contains the default resource value.
1772 if (def_obj && def_item &&
1773 [def_item isEqualToString:[a objectAtIndex:0]])
1774 [a replaceObjectAtIndex:2 withObject:def_obj];
1776 # endif // !USE_IPHONE
1780 # ifdef USE_PICKER_VIEW
1781 /* Finish tweaking the menu button itself.
1784 [popup setTitle:[def_item title]];
1785 NSRect r = [popup frame];
1786 r.size.width = max_width;
1788 # endif // USE_PICKER_VIEW
1791 # if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW)
1792 [self placeChild:popup on:parent];
1793 [self bindResource:popup key:menu_key];
1798 # ifdef USE_PICKER_VIEW
1799 // Store the items for this picker in the picker_values array.
1800 // This is so fucking stupid.
1802 unsigned long menu_number = [pref_keys count] - 1;
1803 if (! picker_values)
1804 picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain];
1805 while ([picker_values count] <= menu_number)
1806 [picker_values addObject:[NSArray arrayWithObjects: nil]];
1807 [picker_values replaceObjectAtIndex:menu_number withObject:items];
1808 [popup reloadAllComponents];
1810 # else // !USE_PICKER_VIEW
1812 [self placeSeparator];
1815 for (__attribute__((unused)) NSArray *item in items) {
1816 RadioButton *b = [[RadioButton alloc] initWithIndex: (int)i
1818 [b setLineBreakMode:NSLineBreakByTruncatingHead];
1819 [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
1820 [self placeChild:b on:parent];
1825 [self placeSeparator];
1827 # endif // !USE_PICKER_VIEW
1828 # endif // !USE_IPHONE
1833 /* Creates an uneditable, wrapping NSTextField to display the given
1834 text enclosed by <description> ... </description> in the XML.
1836 - (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent
1838 NSString *text = nil;
1839 NSArray *children = [node children];
1840 NSUInteger i, count = [children count];
1842 for (i = 0; i < count; i++) {
1843 NSXMLNode *child = [children objectAtIndex:i];
1844 NSString *s = [child objectValue];
1846 text = [text stringByAppendingString:s];
1851 text = unwrap (text);
1853 NSRect rect = [parent frame];
1854 rect.origin.x = rect.origin.y = 0;
1855 rect.size.width = 200;
1856 rect.size.height = 50; // sized later
1858 NSText *lab = [[NSText alloc] initWithFrame:rect];
1860 [lab setEditable:NO];
1861 [lab setDrawsBackground:NO];
1862 [lab setHorizontallyResizable:YES];
1863 [lab setVerticallyResizable:YES];
1864 [lab setString:text];
1869 # else // USE_IPHONE
1871 # ifndef USE_HTML_LABELS
1873 UILabel *lab = [self makeLabel:text];
1874 [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1877 # else // USE_HTML_LABELS
1878 HTMLLabel *lab = [[HTMLLabel alloc]
1880 font:[NSFont systemFontOfSize: [NSFont systemFontSize]]];
1882 [lab setFrame:rect];
1884 # endif // USE_HTML_LABELS
1886 [self placeSeparator];
1888 # endif // USE_IPHONE
1890 [self placeChild:lab on:parent];
1894 /* Creates the NSTextField described by the given XML node.
1896 - (void) makeTextField: (NSXMLNode *)node
1897 on: (NSView *)parent
1898 withLabel: (BOOL) label_p
1899 horizontal: (BOOL) horiz_p
1901 NSMutableDictionary *dict = [@{ @"id": @"",
1905 [self parseAttrs:dict node:node];
1906 NSString *label = [dict objectForKey:@"_label"];
1907 NSString *arg = [dict objectForKey:@"arg"];
1911 if (!label && label_p) {
1912 NSAssert1 (0, @"no _label in %@", [node name]);
1916 NSAssert1 (arg, @"no arg in %@", label);
1919 rect.origin.x = rect.origin.y = 0;
1920 rect.size.width = rect.size.height = 10;
1922 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
1926 // make the default size be around 30 columns; a typical value for
1927 // these text fields is "xscreensaver-text --cols 40".
1929 [txt setStringValue:@"123456789 123456789 123456789 "];
1931 [[txt cell] setWraps:NO];
1932 [[txt cell] setScrollable:YES];
1933 [txt setStringValue:@""];
1935 # else // USE_IPHONE
1937 txt.adjustsFontSizeToFitWidth = YES;
1938 txt.textColor = [UIColor blackColor];
1939 txt.font = [UIFont systemFontOfSize: FONT_SIZE];
1940 txt.placeholder = @"";
1941 txt.borderStyle = UITextBorderStyleRoundedRect;
1942 txt.textAlignment = NSTextAlignmentRight;
1943 txt.keyboardType = UIKeyboardTypeDefault; // Full kbd
1944 txt.autocorrectionType = UITextAutocorrectionTypeNo;
1945 txt.autocapitalizationType = UITextAutocapitalizationTypeNone;
1946 txt.clearButtonMode = UITextFieldViewModeAlways;
1947 txt.returnKeyType = UIReturnKeyDone;
1948 txt.delegate = self;
1950 [txt setEnabled: YES];
1952 rect.size.height = [txt.font lineHeight] * 1.2;
1953 [txt setFrame:rect];
1955 # endif // USE_IPHONE
1958 LABEL *lab = [self makeLabel:label];
1959 [self placeChild:lab on:parent];
1962 [self placeChild:txt on:parent right:(label ? YES : NO)];
1964 [self bindSwitch:txt cmdline:arg];
1969 /* Creates the NSTextField described by the given XML node,
1970 and hooks it up to a Choose button and a file selector widget.
1972 - (void) makeFileSelector: (NSXMLNode *)node
1973 on: (NSView *)parent
1974 dirsOnly: (BOOL) dirsOnly
1975 withLabel: (BOOL) label_p
1976 editable: (BOOL) editable_p
1978 # ifndef USE_IPHONE // No files. No selectors.
1979 NSMutableDictionary *dict = [@{ @"id": @"",
1983 [self parseAttrs:dict node:node];
1984 NSString *label = [dict objectForKey:@"_label"];
1985 NSString *arg = [dict objectForKey:@"arg"];
1989 if (!label && label_p) {
1990 NSAssert1 (0, @"no _label in %@", [node name]);
1994 NSAssert1 (arg, @"no arg in %@", label);
1997 rect.origin.x = rect.origin.y = 0;
1998 rect.size.width = rect.size.height = 10;
2000 NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
2002 // make the default size be around 20 columns.
2004 [txt setStringValue:@"123456789 123456789 "];
2006 [txt setSelectable:YES];
2007 [txt setEditable:editable_p];
2008 [txt setBezeled:editable_p];
2009 [txt setDrawsBackground:editable_p];
2010 [[txt cell] setWraps:NO];
2011 [[txt cell] setScrollable:YES];
2012 [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead];
2013 [txt setStringValue:@""];
2017 lab = [self makeLabel:label];
2018 [self placeChild:lab on:parent];
2021 [self placeChild:txt on:parent right:(label ? YES : NO)];
2023 [self bindSwitch:txt cmdline:arg];
2026 // Make the text field and label be the same height, whichever is taller.
2029 rect.size.height = ([lab frame].size.height > [txt frame].size.height
2030 ? [lab frame].size.height
2031 : [txt frame].size.height);
2032 [txt setFrame:rect];
2035 // Now put a "Choose" button next to it.
2037 rect.origin.x = rect.origin.y = 0;
2038 rect.size.width = rect.size.height = 10;
2039 NSButton *choose = [[NSButton alloc] initWithFrame:rect];
2040 [choose setTitle:@"Choose..."];
2041 [choose setBezelStyle:NSRoundedBezelStyle];
2044 [self placeChild:choose on:parent right:YES];
2046 // center the Choose button around the midpoint of the text field.
2047 rect = [choose frame];
2048 rect.origin.y = ([txt frame].origin.y +
2049 (([txt frame].size.height - rect.size.height) / 2));
2050 [choose setFrameOrigin:rect.origin];
2052 [choose setTarget:[parent window]];
2054 [choose setAction:@selector(fileSelectorChooseDirsAction:)];
2056 [choose setAction:@selector(fileSelectorChooseAction:)];
2059 # endif // !USE_IPHONE
2065 /* Runs a modal file selector and sets the text field's value to the
2066 selected file or directory.
2069 do_file_selector (NSTextField *txt, BOOL dirs_p)
2071 NSOpenPanel *panel = [NSOpenPanel openPanel];
2072 [panel setAllowsMultipleSelection:NO];
2073 [panel setCanChooseFiles:!dirs_p];
2074 [panel setCanChooseDirectories:dirs_p];
2076 NSInteger result = [panel runModal];
2077 if (result == NSOKButton) {
2078 NSArray *files = [panel URLs];
2079 NSString *file = ([files count] > 0 ? [[files objectAtIndex:0] path] : @"");
2080 file = [file stringByAbbreviatingWithTildeInPath];
2081 [txt setStringValue:file];
2083 // Fuck me! Just setting the value of the NSTextField does not cause
2084 // that to end up in the preferences!
2086 NSDictionary *dict = [txt infoForBinding:@"value"];
2087 NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"];
2088 NSString *path = [dict objectForKey:@"NSObservedKeyPath"];
2089 if ([path hasPrefix:@"values."]) // WTF.
2090 path = [path substringFromIndex:7];
2091 [[prefs values] setValue:file forKey:path];
2096 /* Returns the NSTextField that is to the left of or above the NSButton.
2098 static NSTextField *
2099 find_text_field_of_button (NSButton *button)
2101 NSView *parent = [button superview];
2102 NSArray *kids = [parent subviews];
2103 NSUInteger nkids = [kids count];
2106 for (i = 0; i < nkids; i++) {
2107 NSObject *kid = [kids objectAtIndex:i];
2108 if ([kid isKindOfClass:[NSTextField class]]) {
2109 f = (NSTextField *) kid;
2110 } else if (kid == button) {
2119 - (void) fileSelectorChooseAction:(NSObject *)arg
2121 NSButton *choose = (NSButton *) arg;
2122 NSTextField *txt = find_text_field_of_button (choose);
2123 do_file_selector (txt, NO);
2126 - (void) fileSelectorChooseDirsAction:(NSObject *)arg
2128 NSButton *choose = (NSButton *) arg;
2129 NSTextField *txt = find_text_field_of_button (choose);
2130 do_file_selector (txt, YES);
2133 #endif // !USE_IPHONE
2136 - (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2141 (x) Computer name and time
2142 ( ) Text [__________________________]
2143 ( ) Text file [_________________] [Choose]
2144 ( ) URL [__________________________]
2145 ( ) Shell Cmd [__________________________]
2147 textMode -text-mode date
2148 textMode -text-mode literal textLiteral -text-literal %
2149 textMode -text-mode file textFile -text-file %
2150 textMode -text-mode url textURL -text-url %
2151 textMode -text-mode program textProgram -text-program %
2154 rect.size.width = rect.size.height = 1;
2155 rect.origin.x = rect.origin.y = 0;
2156 NSView *group = [[NSView alloc] initWithFrame:rect];
2157 NSView *rgroup = [[NSView alloc] initWithFrame:rect];
2159 Bool program_p = TRUE;
2164 // This is how you link radio buttons together.
2166 NSButtonCell *proto = [[NSButtonCell alloc] init];
2167 [proto setButtonType:NSRadioButton];
2169 rect.origin.x = rect.origin.y = 0;
2170 rect.size.width = rect.size.height = 10;
2171 NSMatrix *matrix = [[NSMatrix alloc]
2173 mode:NSRadioModeMatrix
2175 numberOfRows: 4 + (program_p ? 1 : 0)
2177 [matrix setAllowsEmptySelection:NO];
2179 NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil];
2180 [cnames addObject:@"Computer name and time"];
2181 [cnames addObject:@"Text"];
2182 [cnames addObject:@"File"];
2183 [cnames addObject:@"URL"];
2184 if (program_p) [cnames addObject:@"Shell Cmd"];
2185 [matrix bind:@"content"
2187 withKeyPath:@"arrangedObjects"
2191 [self bindSwitch:matrix cmdline:@"-text-mode %"];
2193 [self placeChild:matrix on:group];
2194 [self placeChild:rgroup on:group right:YES];
2201 # else // USE_IPHONE
2203 NSView *rgroup = parent;
2206 // <select id="textMode">
2207 // <option id="date" _label="Display date" arg-set="-text-mode date"/>
2208 // <option id="text" _label="Display text" arg-set="-text-mode literal"/>
2209 // <option id="url" _label="Display URL"/>
2212 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2213 [node2 setAttributesAsDictionary:@{ @"id": @"textMode" }];
2215 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2216 [node3 setAttributesAsDictionary:
2218 @"arg-set": @"-text-mode date",
2219 @"_label": @"Display the date and time" }];
2220 [node3 setParent: node2];
2221 [node3 autorelease];
2223 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2224 [node3 setAttributesAsDictionary:
2226 @"arg-set": @"-text-mode literal",
2227 @"_label": @"Display static text" }];
2228 [node3 setParent: node2];
2229 [node3 autorelease];
2231 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2232 [node3 setAttributesAsDictionary:
2234 @"_label": @"Display the contents of a URL" }];
2235 [node3 setParent: node2];
2236 [node3 autorelease];
2238 [self makeOptionMenu:node2 on:rgroup];
2241 # endif // USE_IPHONE
2244 // <string id="textLiteral" _label="" arg-set="-text-literal %"/>
2245 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2246 [node2 setAttributesAsDictionary:
2247 @{ @"id": @"textLiteral",
2248 @"arg": @"-text-literal %",
2250 @"_label": @"Text to display"
2253 [self makeTextField:node2 on:rgroup
2262 // rect = [last_child(rgroup) frame];
2264 /* // trying to make the text fields be enabled only when the checkbox is on..
2265 control = last_child (rgroup);
2266 [control bind:@"enabled"
2267 toObject:[matrix cellAtRow:1 column:0]
2268 withKeyPath:@"value"
2274 // <file id="textFile" _label="" arg-set="-text-file %"/>
2275 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2276 [node2 setAttributesAsDictionary:
2277 @{ @"id": @"textFile",
2278 @"arg": @"-text-file %" }];
2279 [self makeFileSelector:node2 on:rgroup
2280 dirsOnly:NO withLabel:NO editable:NO];
2282 # endif // !USE_IPHONE
2284 // rect = [last_child(rgroup) frame];
2286 // <string id="textURL" _label="" arg-set="text-url %"/>
2287 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2288 [node2 setAttributesAsDictionary:
2289 @{ @"id": @"textURL",
2290 @"arg": @"-text-url %",
2292 @"_label": @"URL to display",
2295 [self makeTextField:node2 on:rgroup
2304 // rect = [last_child(rgroup) frame];
2308 // <string id="textProgram" _label="" arg-set="text-program %"/>
2309 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2310 [node2 setAttributesAsDictionary:
2311 @{ @"id": @"textProgram",
2312 @"arg": @"-text-program %",
2314 [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO];
2318 // rect = [last_child(rgroup) frame];
2320 layout_group (rgroup, NO);
2322 rect = [rgroup frame];
2323 rect.size.width += 35; // WTF? Why is rgroup too narrow?
2324 [rgroup setFrame:rect];
2327 // Set the height of the cells in the radio-box matrix to the height of
2328 // the (last of the) text fields.
2329 control = last_child (rgroup);
2330 rect = [control frame];
2331 rect.size.width = 30; // width of the string "Text", plus a bit...
2333 rect.size.width += 25;
2334 rect.size.height += LINE_SPACING;
2335 [matrix setCellSize:rect.size];
2336 [matrix sizeToCells];
2338 layout_group (group, YES);
2339 rect = [matrix frame];
2340 rect.origin.x += rect.size.width + COLUMN_SPACING;
2341 rect.origin.y -= [control frame].size.height - LINE_SPACING;
2342 [rgroup setFrameOrigin:rect.origin];
2344 // now cheat on the size of the matrix: allow it to overlap (underlap)
2347 rect.size = [matrix cellSize];
2348 rect.size.width = 300;
2349 [matrix setCellSize:rect.size];
2350 [matrix sizeToCells];
2352 // Cheat on the position of the stuff on the right (the rgroup).
2353 // GAAAH, this code is such crap!
2354 rect = [rgroup frame];
2356 [rgroup setFrame:rect];
2359 rect.size.width = rect.size.height = 0;
2360 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2361 [box setTitlePosition:NSAtTop];
2362 [box setBorderType:NSBezelBorder];
2363 [box setTitle:@"Display Text"];
2365 rect.size.width = rect.size.height = 12;
2366 [box setContentViewMargins:rect.size];
2367 [box setContentView:group];
2370 [self placeChild:box on:parent];
2374 # endif // !USE_IPHONE
2378 - (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent
2381 [x] Grab desktop images
2382 [ ] Choose random image:
2383 [__________________________] [Choose]
2385 <boolean id="grabDesktopImages" _label="Grab desktop images"
2386 arg-unset="-no-grab-desktop"/>
2387 <boolean id="chooseRandomImages" _label="Grab desktop images"
2388 arg-unset="-choose-random-images"/>
2389 <file id="imageDirectory" _label="" arg-set="-image-directory %"/>
2392 NSXMLElement *node2;
2395 # define SCREENS "Grab desktop images"
2396 # define PHOTOS "Choose random images"
2398 # define SCREENS "Grab screenshots"
2399 # define PHOTOS "Use photo library"
2402 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2403 [node2 setAttributesAsDictionary:
2404 @{ @"id": @"grabDesktopImages",
2405 @"_label": @ SCREENS,
2406 @"arg-unset": @"-no-grab-desktop",
2408 [self makeCheckbox:node2 on:parent];
2411 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2412 [node2 setAttributesAsDictionary:
2413 @{ @"id": @"chooseRandomImages",
2414 @"_label": @ PHOTOS,
2415 @"arg-set": @"-choose-random-images",
2417 [self makeCheckbox:node2 on:parent];
2420 node2 = [[NSXMLElement alloc] initWithName:@"string"];
2421 [node2 setAttributesAsDictionary:
2422 @{ @"id": @"imageDirectory",
2423 @"_label": @"Images from:",
2424 @"arg": @"-image-directory %",
2426 [self makeFileSelector:node2 on:parent
2427 dirsOnly:YES withLabel:YES editable:YES];
2434 // Add a second, explanatory label below the file/URL selector.
2437 lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"];
2438 [self placeChild:lab2 on:parent];
2440 // Pack it in a little tighter vertically.
2441 NSRect r2 = [lab2 frame];
2444 [lab2 setFrameOrigin:r2.origin];
2445 # endif // USE_IPHONE
2449 - (void) makeUpdaterControlBox:(NSXMLNode *)node on:(NSView *)parent
2453 [x] Check for Updates [ Monthly ]
2456 <boolean id="automaticallyChecksForUpdates"
2457 _label="Automatically check for updates"
2458 arg-unset="-no-automaticallyChecksForUpdates" />
2459 <select id="updateCheckInterval">
2460 <option="hourly" _label="Hourly" arg-set="-updateCheckInterval 3600"/>
2461 <option="daily" _label="Daily" arg-set="-updateCheckInterval 86400"/>
2462 <option="weekly" _label="Weekly" arg-set="-updateCheckInterval 604800"/>
2463 <option="monthly" _label="Monthly" arg-set="-updateCheckInterval 2629800"/>
2471 rect.size.width = rect.size.height = 1;
2472 rect.origin.x = rect.origin.y = 0;
2473 NSView *group = [[NSView alloc] initWithFrame:rect];
2475 NSXMLElement *node2;
2479 node2 = [[NSXMLElement alloc] initWithName:@"boolean"];
2480 [node2 setAttributesAsDictionary:
2481 @{ @"id": @SUSUEnableAutomaticChecksKey,
2482 @"_label": @"Automatically check for updates",
2483 @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey,
2485 [self makeCheckbox:node2 on:group];
2490 node2 = [[NSXMLElement alloc] initWithName:@"select"];
2491 [node2 setAttributesAsDictionary:
2492 @{ @"id": @SUScheduledCheckIntervalKey }];
2496 NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"];
2497 [node3 setAttributesAsDictionary:
2498 @{ @"id": @"hourly",
2499 @"arg-set": @"-" SUScheduledCheckIntervalKey " 3600",
2500 @"_label": @"Hourly" }];
2501 [node3 setParent: node2];
2502 [node3 autorelease];
2504 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2505 [node3 setAttributesAsDictionary:
2507 @"arg-set": @"-" SUScheduledCheckIntervalKey " 86400",
2508 @"_label": @"Daily" }];
2509 [node3 setParent: node2];
2510 [node3 autorelease];
2512 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2513 [node3 setAttributesAsDictionary:
2514 @{ @"id": @"weekly",
2515 // @"arg-set": @"-" SUScheduledCheckIntervalKey " 604800",
2516 @"_label": @"Weekly",
2518 [node3 setParent: node2];
2519 [node3 autorelease];
2521 node3 = [[NSXMLElement alloc] initWithName:@"option"];
2522 [node3 setAttributesAsDictionary:
2523 @{ @"id": @"monthly",
2524 @"arg-set": @"-" SUScheduledCheckIntervalKey " 2629800",
2525 @"_label": @"Monthly",
2527 [node3 setParent: node2];
2528 [node3 autorelease];
2531 [self makeOptionMenu:node2 on:group];
2535 layout_group (group, TRUE);
2537 rect.size.width = rect.size.height = 0;
2538 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2539 [box setTitlePosition:NSNoTitle];
2540 [box setBorderType:NSNoBorder];
2541 [box setContentViewMargins:rect.size];
2542 [box setContentView:group];
2545 [self placeChild:box on:parent];
2550 # endif // !USE_IPHONE
2554 #pragma mark Layout for controls
2559 last_child (NSView *parent)
2561 NSArray *kids = [parent subviews];
2562 NSUInteger nkids = [kids count];
2566 return [kids objectAtIndex:nkids-1];
2568 #endif // USE_IPHONE
2571 /* Add the child as a subview of the parent, positioning it immediately
2572 below or to the right of the previously-added child of that view.
2574 - (void) placeChild:
2580 on:(NSView *)parent right:(BOOL)right_p
2583 NSRect rect = [child frame];
2584 NSView *last = last_child (parent);
2586 rect.origin.x = LEFT_MARGIN;
2587 rect.origin.y = ([parent frame].size.height - rect.size.height
2589 } else if (right_p) {
2590 rect = [last frame];
2591 rect.origin.x += rect.size.width + COLUMN_SPACING;
2593 rect = [last frame];
2594 rect.origin.x = LEFT_MARGIN;
2595 rect.origin.y -= [child frame].size.height + LINE_SPACING;
2597 NSRect r = [child frame];
2598 r.origin = rect.origin;
2600 [parent addSubview:child];
2602 # else // USE_IPHONE
2604 /* Controls is an array of arrays of the controls, divided into sections.
2605 Each hgroup / vgroup gets a nested array, too, e.g.:
2607 [ [ [ <label>, <checkbox> ],
2608 [ <label>, <checkbox> ],
2609 [ <label>, <checkbox> ] ],
2610 [ <label>, <text-field> ],
2611 [ <label>, <low-label>, <slider>, <high-label> ],
2612 [ <low-label>, <slider>, <high-label> ],
2616 If an element begins with a label, it is terminal, otherwise it is a
2617 group. There are (currently) never more than 4 elements in a single
2620 A blank vertical spacer is placed between each hgroup / vgroup,
2621 by making each of those a new section in the TableView.
2624 controls = [[NSMutableArray arrayWithCapacity:10] retain];
2625 if ([controls count] == 0)
2626 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2627 NSMutableArray *current = [controls objectAtIndex:[controls count]-1];
2629 if (!right_p || [current count] == 0) {
2630 // Nothing on the current line. Add this object.
2631 [current addObject: child];
2633 // Something's on the current line already.
2634 NSObject *old = [current objectAtIndex:[current count]-1];
2635 if ([old isKindOfClass:[NSMutableArray class]]) {
2636 // Already an array in this cell. Append.
2637 NSAssert ([(NSArray *) old count] < 4, @"internal error");
2638 [(NSMutableArray *) old addObject: child];
2640 // Replace the control in this cell with an array, then append
2641 NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil];
2642 [current replaceObjectAtIndex:[current count]-1 withObject:a];
2645 # endif // USE_IPHONE
2649 - (void) placeChild:(NSView *)child on:(NSView *)parent
2651 [self placeChild:child on:parent right:NO];
2657 // Start putting subsequent children in a new group, to create a new
2658 // section on the UITableView.
2660 - (void) placeSeparator
2662 if (! controls) return;
2663 if ([controls count] == 0) return;
2664 if ([[controls objectAtIndex:[controls count]-1]
2666 [controls addObject: [NSMutableArray arrayWithCapacity:10]];
2668 #endif // USE_IPHONE
2672 /* Creates an invisible NSBox (for layout purposes) to enclose the widgets
2673 wrapped in <hgroup> or <vgroup> in the XML.
2675 - (void) makeGroup:(NSXMLNode *)node
2677 horizontal:(BOOL) horiz_p
2680 if (!horiz_p) [self placeSeparator];
2681 [self traverseChildren:node on:parent];
2682 if (!horiz_p) [self placeSeparator];
2683 # else // !USE_IPHONE
2685 rect.size.width = rect.size.height = 1;
2686 rect.origin.x = rect.origin.y = 0;
2687 NSView *group = [[NSView alloc] initWithFrame:rect];
2688 [self traverseChildren:node on:group];
2690 layout_group (group, horiz_p);
2692 rect.size.width = rect.size.height = 0;
2693 NSBox *box = [[NSBox alloc] initWithFrame:rect];
2694 [box setTitlePosition:NSNoTitle];
2695 [box setBorderType:NSNoBorder];
2696 [box setContentViewMargins:rect.size];
2697 [box setContentView:group];
2700 [self placeChild:box on:parent];
2703 # endif // !USE_IPHONE
2709 layout_group (NSView *group, BOOL horiz_p)
2711 NSArray *kids = [group subviews];
2712 NSUInteger nkids = [kids count];
2714 double maxx = 0, miny = 0;
2715 for (i = 0; i < nkids; i++) {
2716 NSView *kid = [kids objectAtIndex:i];
2717 NSRect r = [kid frame];
2720 maxx += r.size.width + COLUMN_SPACING;
2721 if (r.size.height > -miny) miny = -r.size.height;
2723 if (r.size.width > maxx) maxx = r.size.width;
2724 miny = r.origin.y - r.size.height;
2731 rect.size.width = maxx;
2732 rect.size.height = -miny;
2733 [group setFrame:rect];
2736 for (i = 0; i < nkids; i++) {
2737 NSView *kid = [kids objectAtIndex:i];
2738 NSRect r = [kid frame];
2740 r.origin.y = rect.size.height - r.size.height;
2742 x += r.size.width + COLUMN_SPACING;
2749 #endif // !USE_IPHONE
2752 /* Create some kind of control corresponding to the given XML node.
2754 -(void)makeControl:(NSXMLNode *)node on:(NSView *)parent
2756 NSString *name = [node name];
2758 if ([node kind] == NSXMLCommentKind)
2761 if ([node kind] == NSXMLTextKind) {
2762 NSString *s = [(NSString *) [node objectValue]
2763 stringByTrimmingCharactersInSet:
2764 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
2765 if (! [s isEqualToString:@""]) {
2766 NSAssert1 (0, @"unexpected text: %@", s);
2771 if ([node kind] != NSXMLElementKind) {
2772 NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node);
2776 if ([name isEqualToString:@"hgroup"] ||
2777 [name isEqualToString:@"vgroup"]) {
2779 [self makeGroup:node on:parent
2780 horizontal:[name isEqualToString:@"hgroup"]];
2782 } else if ([name isEqualToString:@"command"]) {
2783 // do nothing: this is the "-root" business
2785 } else if ([name isEqualToString:@"video"]) {
2788 } else if ([name isEqualToString:@"boolean"]) {
2789 [self makeCheckbox:node on:parent];
2791 } else if ([name isEqualToString:@"string"]) {
2792 [self makeTextField:node on:parent withLabel:NO horizontal:NO];
2794 } else if ([name isEqualToString:@"file"]) {
2795 [self makeFileSelector:node on:parent
2796 dirsOnly:NO withLabel:YES editable:NO];
2798 } else if ([name isEqualToString:@"number"]) {
2799 [self makeNumberSelector:node on:parent];
2801 } else if ([name isEqualToString:@"select"]) {
2802 [self makeOptionMenu:node on:parent];
2804 } else if ([name isEqualToString:@"_description"]) {
2805 [self makeDescLabel:node on:parent];
2807 } else if ([name isEqualToString:@"xscreensaver-text"]) {
2808 [self makeTextLoaderControlBox:node on:parent];
2810 } else if ([name isEqualToString:@"xscreensaver-image"]) {
2811 [self makeImageLoaderControlBox:node on:parent];
2813 } else if ([name isEqualToString:@"xscreensaver-updater"]) {
2814 [self makeUpdaterControlBox:node on:parent];
2817 NSAssert1 (0, @"unknown tag: %@", name);
2822 /* Iterate over and process the children of this XML node.
2824 - (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent
2826 NSArray *children = [node children];
2827 NSUInteger i, count = [children count];
2828 for (i = 0; i < count; i++) {
2829 NSXMLNode *child = [children objectAtIndex:i];
2830 [self makeControl:child on:parent];
2837 /* Kludgey magic to make the window enclose the controls we created.
2840 fix_contentview_size (NSView *parent)
2843 NSArray *kids = [parent subviews];
2844 NSUInteger nkids = [kids count];
2845 NSView *text = 0; // the NSText at the bottom of the window
2846 double maxx = 0, miny = 0;
2849 /* Find the size of the rectangle taken up by each of the children
2850 except the final "NSText" child.
2852 for (i = 0; i < nkids; i++) {
2853 NSView *kid = [kids objectAtIndex:i];
2854 if ([kid isKindOfClass:[NSText class]]) {
2859 if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width;
2860 if (f.origin.y - f.size.height < miny) miny = f.origin.y;
2861 // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2862 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2863 // f.origin.y + f.size.height, [kid class]);
2866 if (maxx < 400) maxx = 400; // leave room for the NSText paragraph...
2868 /* Now that we know the width of the window, set the width of the NSText to
2869 that, so that it can decide what its height needs to be.
2871 if (! text) abort();
2873 // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2874 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2875 // f.origin.y + f.size.height, [text class]);
2877 // set the NSText's width (this changes its height).
2878 f.size.width = maxx - LEFT_MARGIN;
2881 // position the NSText below the last child (this gives us a new miny).
2883 f.origin.y = miny - f.size.height - LINE_SPACING;
2884 miny = f.origin.y - LINE_SPACING;
2887 // Lock the width of the field and unlock the height, and let it resize
2888 // once more, to compute the proper height of the text for that width.
2890 [(NSText *) text setHorizontallyResizable:NO];
2891 [(NSText *) text setVerticallyResizable:YES];
2892 [(NSText *) text sizeToFit];
2894 // Now lock the height too: no more resizing this text field.
2896 [(NSText *) text setVerticallyResizable:NO];
2898 // Now reposition the top edge of the text field to be back where it
2899 // was before we changed the height.
2901 float oh = f.size.height;
2903 float dh = f.size.height - oh;
2906 // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF??
2907 // If we do this in 10.6, the text field moves down, off the window.
2908 // So instead we repair it at the end, at the "WTF2" comment.
2911 // Also adjust the parent height by the change in height of the text field.
2914 // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2915 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2916 // f.origin.y + f.size.height, [text class]);
2919 /* Set the contentView to the size of the children.
2922 // float yoff = f.size.height;
2923 f.size.width = maxx + LEFT_MARGIN;
2924 f.size.height = -(miny - LEFT_MARGIN*2);
2925 // yoff = f.size.height - yoff;
2926 [parent setFrame:f];
2928 // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f",
2929 // f.size.width, f.size.height, f.origin.x, f.origin.y);
2931 /* Now move all of the kids up into the window.
2934 float shift = f.size.height;
2935 // NSLog(@"shift: %3.0f", shift);
2936 for (i = 0; i < nkids; i++) {
2937 NSView *kid = [kids objectAtIndex:i];
2939 f.origin.y += shift;
2941 // NSLog(@"move: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@",
2942 // f.size.width, f.size.height, f.origin.x, f.origin.y,
2943 // f.origin.y + f.size.height, [kid class]);
2948 parent: 420 x 541 @ 0 0
2949 text: 380 x 100 @ 20 22 miny=-501
2952 parent: 420 x 541 @ 0 0
2953 text: 380 x 100 @ 20 50 miny=-501
2956 // #### WTF2: See "WTF" above. If the text field is off the screen,
2957 // move it up. We need this on 10.6 but not on 10.5. Auugh.
2960 if (f.origin.y < 50) { // magic numbers, yay
2965 /* Set the kids to track the top left corner of the window when resized.
2966 Set the NSText to track the bottom right corner as well.
2968 for (i = 0; i < nkids; i++) {
2969 NSView *kid = [kids objectAtIndex:i];
2970 unsigned long mask = NSViewMaxXMargin | NSViewMinYMargin;
2971 if ([kid isKindOfClass:[NSText class]])
2972 mask |= NSViewWidthSizable|NSViewHeightSizable;
2973 [kid setAutoresizingMask:mask];
2976 # endif // !USE_IPHONE
2982 wrap_with_buttons (NSWindow *window, NSView *panel)
2986 // Make a box to hold the buttons at the bottom of the window.
2988 rect = [panel frame];
2989 rect.origin.x = rect.origin.y = 0;
2990 rect.size.height = 10;
2991 NSBox *bbox = [[NSBox alloc] initWithFrame:rect];
2992 [bbox setTitlePosition:NSNoTitle];
2993 [bbox setBorderType:NSNoBorder];
2995 // Make some buttons: Default, Cancel, OK
2997 rect.origin.x = rect.origin.y = 0;
2998 rect.size.width = rect.size.height = 10;
2999 NSButton *reset = [[NSButton alloc] initWithFrame:rect];
3000 [reset setTitle:@"Reset to Defaults"];
3001 [reset setBezelStyle:NSRoundedBezelStyle];
3004 rect = [reset frame];
3005 NSButton *ok = [[NSButton alloc] initWithFrame:rect];
3006 [ok setTitle:@"OK"];
3007 [ok setBezelStyle:NSRoundedBezelStyle];
3009 rect = [bbox frame];
3010 rect.origin.x = rect.size.width - [ok frame].size.width;
3011 [ok setFrameOrigin:rect.origin];
3014 NSButton *cancel = [[NSButton alloc] initWithFrame:rect];
3015 [cancel setTitle:@"Cancel"];
3016 [cancel setBezelStyle:NSRoundedBezelStyle];
3018 rect.origin.x -= [cancel frame].size.width + 10;
3019 [cancel setFrameOrigin:rect.origin];
3021 // Bind OK to RET and Cancel to ESC.
3022 [ok setKeyEquivalent:@"\r"];
3023 [cancel setKeyEquivalent:@"\e"];
3025 // The correct width for OK and Cancel buttons is 68 pixels
3026 // ("Human Interface Guidelines: Controls: Buttons:
3027 // Push Button Specifications").
3030 rect.size.width = 68;
3033 rect = [cancel frame];
3034 rect.size.width = 68;
3035 [cancel setFrame:rect];
3037 // It puts the buttons in the box or else it gets the hose again
3039 [bbox addSubview:ok];
3040 [bbox addSubview:cancel];
3041 [bbox addSubview:reset];
3044 // make a box to hold the button-box, and the preferences view
3046 rect = [bbox frame];
3047 rect.origin.y += rect.size.height;
3048 NSBox *pbox = [[NSBox alloc] initWithFrame:rect];
3049 [pbox setTitlePosition:NSNoTitle];
3050 [pbox setBorderType:NSBezelBorder];
3052 // Enforce a max height on the dialog, so that it's obvious to me
3053 // (on a big screen) when the dialog will fall off the bottom of
3054 // a small screen (e.g., 1024x768 laptop with a huge bottom dock).
3056 NSRect f = [panel frame];
3057 int screen_height = (768 // shortest "modern" Mac display
3059 - 56 // System Preferences toolbar
3060 - 140 // default magnified bottom dock icon
3062 if (f.size.height > screen_height) {
3063 NSLog(@"%@ height was %.0f; clipping to %d",
3064 [panel class], f.size.height, screen_height);
3065 f.size.height = screen_height;
3070 [pbox addSubview:panel];
3071 [pbox addSubview:bbox];
3074 [reset setAutoresizingMask:NSViewMaxXMargin];
3075 [cancel setAutoresizingMask:NSViewMinXMargin];
3076 [ok setAutoresizingMask:NSViewMinXMargin];
3077 [bbox setAutoresizingMask:NSViewWidthSizable];
3081 [ok setTarget:window];
3082 [cancel setTarget:window];
3083 [reset setTarget:window];
3084 [ok setAction:@selector(okAction:)];
3085 [cancel setAction:@selector(cancelAction:)];
3086 [reset setAction:@selector(resetAction:)];
3092 #endif // !USE_IPHONE
3095 /* Iterate over and process the children of the root node of the XML document.
3097 - (void)traverseTree
3100 NSView *parent = [self view];
3102 NSWindow *parent = self;
3104 NSXMLNode *node = xml_root;
3106 if (![[node name] isEqualToString:@"screensaver"]) {
3107 NSAssert (0, @"top level node is not <xscreensaver>");
3110 saver_name = [self parseXScreenSaverTag: node];
3111 saver_name = [saver_name stringByReplacingOccurrencesOfString:@" "
3113 [saver_name retain];
3118 rect.origin.x = rect.origin.y = 0;
3119 rect.size.width = rect.size.height = 1;
3121 NSView *panel = [[NSView alloc] initWithFrame:rect];
3122 [self traverseChildren:node on:panel];
3123 fix_contentview_size (panel);
3125 NSView *root = wrap_with_buttons (parent, panel);
3126 [userDefaultsController setAppliesImmediately:NO];
3127 [globalDefaultsController setAppliesImmediately:NO];
3129 [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
3131 rect = [parent frameRectForContentRect:[root frame]];
3132 [parent setFrame:rect display:NO];
3133 [parent setMinSize:rect.size];
3135 [parent setContentView:root];
3140 # else // USE_IPHONE
3142 CGRect r = [parent frame];
3143 r.size = [[UIScreen mainScreen] bounds].size;
3144 [parent setFrame:r];
3145 [self traverseChildren:node on:parent];
3147 # endif // USE_IPHONE
3151 - (void)parser:(NSXMLParser *)parser
3152 didStartElement:(NSString *)elt
3153 namespaceURI:(NSString *)ns
3154 qualifiedName:(NSString *)qn
3155 attributes:(NSDictionary *)attrs
3157 NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt];
3159 [e setKind:SimpleXMLElementKind];
3160 [e setAttributesAsDictionary:attrs];
3161 NSXMLElement *p = xml_parsing;
3165 xml_root = xml_parsing;
3168 - (void)parser:(NSXMLParser *)parser
3169 didEndElement:(NSString *)elt
3170 namespaceURI:(NSString *)ns
3171 qualifiedName:(NSString *)qn
3173 NSXMLElement *p = xml_parsing;
3175 NSLog(@"extra close: %@", elt);
3176 } else if (![[p name] isEqualToString:elt]) {
3177 NSLog(@"%@ closed by %@", [p name], elt);
3179 NSXMLElement *n = xml_parsing;
3180 xml_parsing = [n parent];
3185 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
3187 NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"];
3188 [e setKind:SimpleXMLTextKind];
3189 NSXMLElement *p = xml_parsing;
3191 [e setObjectValue: string];
3197 # ifdef USE_PICKER_VIEW
3199 #pragma mark UIPickerView delegate methods
3201 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv
3203 return 1; // Columns
3206 - (NSInteger)pickerView:(UIPickerView *)pv
3207 numberOfRowsInComponent:(NSInteger)column
3209 NSAssert (column == 0, @"weird column");
3210 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3211 if (! a) return 0; // Too early?
3215 - (CGFloat)pickerView:(UIPickerView *)pv
3216 rowHeightForComponent:(NSInteger)column
3221 - (CGFloat)pickerView:(UIPickerView *)pv
3222 widthForComponent:(NSInteger)column
3224 NSAssert (column == 0, @"weird column");
3225 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3226 if (! a) return 0; // Too early?
3228 UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]];
3230 for (NSArray *a2 in a) {
3231 NSString *s = [a2 objectAtIndex:0];
3232 // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize.
3233 CGSize r = [s sizeWithFont:f];
3234 if (r.width > max) max = r.width;
3237 max *= 1.7; // WTF!!
3249 - (NSString *)pickerView:(UIPickerView *)pv
3250 titleForRow:(NSInteger)row
3251 forComponent:(NSInteger)column
3253 NSAssert (column == 0, @"weird column");
3254 NSArray *a = [picker_values objectAtIndex: [pv tag]];
3255 if (! a) return 0; // Too early?
3256 a = [a objectAtIndex:row];
3257 NSAssert (a, @"internal error");
3258 return [a objectAtIndex:0];
3261 # endif // USE_PICKER_VIEW
3264 #pragma mark UITableView delegate methods
3266 - (void) addResetButton
3268 [[self navigationItem]
3269 setRightBarButtonItem: [[UIBarButtonItem alloc]
3270 initWithTitle: @"Reset to Defaults"
3271 style: UIBarButtonItemStylePlain
3273 action:@selector(resetAction:)]];
3274 NSString *s = saver_name;
3275 if ([self view].frame.size.width > 320)
3276 s = [s stringByAppendingString: @" Settings"];
3277 [self navigationItem].title = s;
3281 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
3286 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tv {
3287 // Number of vertically-stacked white boxes.
3288 return [controls count];
3291 - (NSInteger)tableView:(UITableView *)tableView
3292 numberOfRowsInSection:(NSInteger)section
3294 // Number of lines in each vertically-stacked white box.
3295 NSAssert (controls, @"internal error");
3296 return [[controls objectAtIndex:section] count];
3299 - (NSString *)tableView:(UITableView *)tv
3300 titleForHeaderInSection:(NSInteger)section
3302 // Titles above each vertically-stacked white box.
3303 // if (section == 0)
3304 // return [saver_name stringByAppendingString:@" Settings"];
3309 - (CGFloat)tableView:(UITableView *)tv
3310 heightForRowAtIndexPath:(NSIndexPath *)ip
3314 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3315 objectAtIndex:[ip indexAtPosition:1]];
3317 if ([ctl isKindOfClass:[NSArray class]]) {
3318 NSArray *set = (NSArray *) ctl;
3319 switch ([set count]) {
3320 case 4: // label + left/slider/right.
3321 case 3: // left/slider/right.
3322 h = FONT_SIZE * 3.0;
3324 case 2: // Checkboxes, or text fields.
3325 h = FONT_SIZE * 2.4;
3328 } else if ([ctl isKindOfClass:[UILabel class]]) {
3329 // Radio buttons in a multi-select list.
3330 h = FONT_SIZE * 1.9;
3332 # ifdef USE_HTML_LABELS
3333 } else if ([ctl isKindOfClass:[HTMLLabel class]]) {
3335 HTMLLabel *t = (HTMLLabel *) ctl;
3337 r.size.width = [tv frame].size.width;
3338 r.size.width -= LEFT_MARGIN * 2;
3343 # endif // USE_HTML_LABELS
3345 } else { // Does this ever happen?
3346 h = FONT_SIZE + LINE_SPACING * 2;
3349 if (h <= 0) abort();
3354 - (void)refreshTableView
3356 UITableView *tv = (UITableView *) [self view];
3357 NSMutableArray *a = [NSMutableArray arrayWithCapacity:20];
3358 NSInteger rows = [self numberOfSectionsInTableView:tv];
3359 for (int i = 0; i < rows; i++) {
3360 NSInteger cols = [self tableView:tv numberOfRowsInSection:i];
3361 for (int j = 0; j < cols; j++) {
3365 [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]];
3370 [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone];
3373 // Default opacity looks bad.
3374 // #### Oh great, this only works *sometimes*.
3375 UIView *v = [[self navigationItem] titleView];
3376 [v setBackgroundColor:[[v backgroundColor] colorWithAlphaComponent:1]];
3380 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o
3382 [NSTimer scheduledTimerWithTimeInterval: 0
3384 selector:@selector(refreshTableView)
3390 #ifndef USE_PICKER_VIEW
3392 - (void)updateRadioGroupCell:(UITableViewCell *)cell
3393 button:(RadioButton *)b
3395 NSArray *item = [[b items] objectAtIndex: [b index]];
3396 NSString *pref_key = [item objectAtIndex:1];
3397 NSObject *pref_val = [item objectAtIndex:2];
3399 NSObject *current = [[self controllerForKey:pref_key] objectForKey:pref_key];
3401 // Convert them both to strings and compare those, so that
3402 // we don't get screwed by int 1 versus string "1".
3403 // Will boolean true/1 screw us here too?
3405 NSString *pref_str = ([pref_val isKindOfClass:[NSString class]]
3406 ? (NSString *) pref_val
3407 : [(NSNumber *) pref_val stringValue]);
3408 NSString *current_str = ([current isKindOfClass:[NSString class]]
3409 ? (NSString *) current
3410 : [(NSNumber *) current stringValue]);
3411 BOOL match_p = [current_str isEqualToString:pref_str];
3413 // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str);
3416 [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
3418 [cell setAccessoryType:UITableViewCellAccessoryNone];
3422 - (void)tableView:(UITableView *)tv
3423 didSelectRowAtIndexPath:(NSIndexPath *)ip
3425 RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3426 objectAtIndex:[ip indexAtPosition:1]];
3427 if (! [ctl isKindOfClass:[RadioButton class]])
3430 [self radioAction:ctl];
3431 [self refreshTableView];
3435 #endif // !USE_PICKER_VIEW
3439 - (UITableViewCell *)tableView:(UITableView *)tv
3440 cellForRowAtIndexPath:(NSIndexPath *)ip
3442 CGFloat ww = [tv frame].size.width;
3443 CGFloat hh = [self tableView:tv heightForRowAtIndexPath:ip];
3445 float os_version = [[[UIDevice currentDevice] systemVersion] floatValue];
3447 // Width of the column of labels on the left.
3448 CGFloat left_width = ww * 0.4;
3449 CGFloat right_edge = ww - LEFT_MARGIN;
3451 if (os_version < 7) // margins were wider on iOS 6.1
3454 CGFloat max = FONT_SIZE * 12;
3455 if (left_width > max) left_width = max;
3457 NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]]
3458 objectAtIndex:[ip indexAtPosition:1]];
3460 if ([ctl isKindOfClass:[NSArray class]]) {
3461 // This cell has a set of objects in it.
3462 NSArray *set = (NSArray *) ctl;
3463 switch ([set count]) {
3466 // With 2 elements, the first of the pair must be a label.
3467 UILabel *label = (UILabel *) [set objectAtIndex: 0];
3468 NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type");
3469 ctl = [set objectAtIndex: 1];
3471 CGRect r = [ctl frame];
3473 if ([ctl isKindOfClass:[UISwitch class]]) { // Checkboxes.
3474 r.size.width = 80; // Magic.
3475 r.origin.x = right_edge - r.size.width + 30; // beats me
3477 if (os_version < 7) // checkboxes were wider on iOS 6.1
3481 r.origin.x = left_width; // Text fields, etc.
3482 r.size.width = right_edge - r.origin.x;
3485 r.origin.y = (hh - r.size.height) / 2; // Center vertically.
3488 // Make a box and put the label and checkbox/slider into it.
3493 NSView *box = [[UIView alloc] initWithFrame:r];
3494 [box addSubview: ctl];
3496 // Let the label make use of any space not taken up by the control.
3498 r.origin.x = LEFT_MARGIN;
3500 r.size.width = [ctl frame].origin.x - r.origin.x;
3503 [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]];
3504 [box addSubview: label];
3513 // With 3 elements, 1 and 3 are labels.
3514 // With 4 elements, 1, 2 and 4 are labels.
3516 UILabel *top = ([set count] == 4
3517 ? [set objectAtIndex: i++]
3519 UILabel *left = [set objectAtIndex: i++];
3520 NSView *mid = [set objectAtIndex: i++];
3521 UILabel *right = [set objectAtIndex: i++];
3522 NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF");
3523 NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF");
3524 NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF");
3525 NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF");
3527 // 3 elements: control at top of cell.
3528 // 4 elements: center the control vertically.
3529 CGRect r = [mid frame];
3530 r.size.height = 32; // Unchangable height of the slider thumb.
3532 // Center the slider between left_width and right_edge.
3533 # ifdef LABEL_ABOVE_SLIDER
3534 r.origin.x = LEFT_MARGIN;
3536 r.origin.x = left_width;
3538 r.origin.y = (hh - r.size.height) / 2;
3539 r.size.width = right_edge - r.origin.x;
3543 # ifdef LABEL_ABOVE_SLIDER
3544 // Top label goes above, flush center/top.
3545 r.origin.x = (ww - r.size.width) / 2;
3547 // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize.
3548 r.size = [[top text] sizeWithFont:[top font]
3550 CGSizeMake (ww - LEFT_MARGIN*2, 100000)
3551 lineBreakMode:[top lineBreakMode]];
3552 # else // !LABEL_ABOVE_SLIDER
3553 // Label goes on the left.
3554 r.origin.x = LEFT_MARGIN;
3556 r.size.width = left_width - LEFT_MARGIN;
3558 # endif // !LABEL_ABOVE_SLIDER
3562 // Left label goes under control, flush left/bottom.
3563 left.frame = CGRectMake([mid frame].origin.x, hh - 4,
3564 ww - LEFT_MARGIN*2, 100000);
3567 r.origin.y -= r.size.height;
3570 // Right label goes under control, flush right/bottom.
3572 CGRectMake([mid frame].origin.x + [mid frame].size.width,
3573 [left frame].origin.y, ww - LEFT_MARGIN*2, 1000000);
3576 r.origin.x -= r.size.width;
3579 // Make a box and put the labels and slider into it.
3584 NSView *box = [[UIView alloc] initWithFrame:r];
3586 [box addSubview: top];
3587 [box addSubview: left];
3588 [box addSubview: right];
3589 [box addSubview: mid];
3596 NSAssert (0, @"unhandled size");
3598 } else { // A single view, not a pair.
3599 CGRect r = [ctl frame];
3600 r.origin.x = LEFT_MARGIN;
3602 r.size.width = right_edge - r.origin.x;
3607 NSString *id = @"Cell";
3608 UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:id];
3610 cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault
3611 reuseIdentifier: id]
3614 for (UIView *subview in [cell.contentView subviews])
3615 [subview removeFromSuperview];
3616 [cell.contentView addSubview: ctl];
3617 CGRect r = [ctl frame];
3621 cell.selectionStyle = UITableViewCellSelectionStyleNone;
3622 [cell setAccessoryType:UITableViewCellAccessoryNone];
3624 # ifndef USE_PICKER_VIEW
3625 if ([ctl isKindOfClass:[RadioButton class]])
3626 [self updateRadioGroupCell:cell button:(RadioButton *)ctl];
3627 # endif // USE_PICKER_VIEW
3631 # endif // USE_IPHONE
3634 /* When this object is instantiated, it parses the XML file and creates
3635 controls on itself that are hooked up to the appropriate preferences.
3636 The default size of the view is just big enough to hold them all.
3638 - (id)initWithXML: (NSData *) xml_data
3639 options: (const XrmOptionDescRec *) _opts
3640 controller: (NSUserDefaultsController *) _prefs
3641 globalController: (NSUserDefaultsController *) _globalPrefs
3642 defaults: (NSDictionary *) _defs
3645 self = [super init];
3646 # else // !USE_IPHONE
3647 self = [super initWithStyle:UITableViewStyleGrouped];
3648 self.title = [saver_name stringByAppendingString:@" Settings"];
3649 # endif // !USE_IPHONE
3650 if (! self) return 0;
3652 // instance variables
3654 defaultOptions = _defs;
3655 userDefaultsController = [_prefs retain];
3656 globalDefaultsController = [_globalPrefs retain];
3658 NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithData:xml_data];
3661 NSAssert1 (0, @"XML Error: %@",
3662 [[NSString alloc] initWithData:xml_data
3663 encoding:NSUTF8StringEncoding]);
3666 [xmlDoc setDelegate:self];
3667 if (! [xmlDoc parse]) {
3668 NSError *err = [xmlDoc parserError];
3669 NSAssert2 (0, @"XML Error: %@: %@",
3670 [[NSString alloc] initWithData:xml_data
3671 encoding:NSUTF8StringEncoding],
3677 TextModeTransformer *t = [[TextModeTransformer alloc] init];
3678 [NSValueTransformer setValueTransformer:t
3679 forName:@"TextModeTransformer"];
3681 # endif // USE_IPHONE
3683 [self traverseTree];
3687 [self addResetButton];
3696 [saver_name release];
3697 [userDefaultsController release];
3698 [globalDefaultsController release];
3701 [pref_keys release];
3702 [pref_ctls release];
3703 # ifdef USE_PICKER_VIEW
3704 [picker_values release];