+{
+ NSString *html;
+ UIFont *font;
+ UIWebView *webView;
+}
+
+@property(nonatomic, retain) NSString *html;
+@property(nonatomic, retain) UIWebView *webView;
+
+- (id) initWithHTML:(NSString *)h font:(UIFont *)f;
+- (id) initWithText:(NSString *)t font:(UIFont *)f;
+- (void) setHTML:(NSString *)h;
+- (void) setText:(NSString *)t;
+- (void) sizeToFit;
+
+@end
+
+@implementation HTMLLabel
+
+@synthesize html;
+@synthesize webView;
+
+- (id) initWithHTML:(NSString *)h font:(UIFont *)f
+{
+ self = [super init];
+ if (! self) return 0;
+ font = [f retain];
+ webView = [[UIWebView alloc] init];
+ webView.delegate = self;
+ webView.dataDetectorTypes = UIDataDetectorTypeNone;
+ self. autoresizingMask = UIViewAutoresizingNone; // we do it manually
+ webView.autoresizingMask = UIViewAutoresizingNone;
+ webView.scrollView.scrollEnabled = NO;
+ webView.scrollView.bounces = NO;
+ webView.opaque = NO;
+ [webView setBackgroundColor:[UIColor clearColor]];
+
+ [self addSubview: webView];
+ [self setHTML: h];
+ return self;
+}
+
+- (id) initWithText:(NSString *)t font:(UIFont *)f
+{
+ self = [self initWithHTML:@"" font:f];
+ if (! self) return 0;
+ [self setText: t];
+ return self;
+}
+
+
+- (void) setHTML: (NSString *)h
+{
+ if (! h) return;
+ [h retain];
+ if (html) [html release];
+ html = h;
+ NSString *h2 =
+ [NSString stringWithFormat:
+ @""
+ ""
+ ""
+// ""
+ ""
+ ""
+ ""
+ "%@"
+ ""
+ "",
+ [font fontName],
+ [font pointSize],
+ [font lineHeight],
+ h];
+ [webView stopLoading];
+ [webView loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]];
+}
+
+
+static char *anchorize (const char *url);
+
+- (void) setText: (NSString *)t
+{
+ t = [t stringByTrimmingCharactersInSet:[NSCharacterSet
+ whitespaceCharacterSet]];
+ t = [t stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
+ t = [t stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
+ t = [t stringByReplacingOccurrencesOfString:@">" withString:@">"];
+ t = [t stringByReplacingOccurrencesOfString:@"\n\n" withString:@" "];
+ t = [t stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"];
+ t = [t stringByReplacingOccurrencesOfString:@"\n "
+ withString:@"
"];
+
+ NSString *h = @"";
+ for (NSString *s in
+ [t componentsSeparatedByCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
+ if ([s hasPrefix:@"http://"] ||
+ [s hasPrefix:@"https://"]) {
+ char *anchor = anchorize ([s cStringUsingEncoding:NSUTF8StringEncoding]);
+ NSString *a2 = [NSString stringWithCString: anchor
+ encoding: NSUTF8StringEncoding];
+ s = [NSString stringWithFormat: @"%@
", s, a2];
+ free (anchor);
+ }
+ h = [NSString stringWithFormat: @"%@ %@", h, s];
+ }
+
+ h = [h stringByReplacingOccurrencesOfString:@"
" withString:@"
"];
+ h = [h stringByReplacingOccurrencesOfString:@"
" withString:@"
"];
+ h = [h stringByTrimmingCharactersInSet:[NSCharacterSet
+ whitespaceAndNewlineCharacterSet]];
+
+ [self setHTML: h];
+}
+
+
+-(BOOL) webView:(UIWebView *)wv
+ shouldStartLoadWithRequest:(NSURLRequest *)req
+ navigationType:(UIWebViewNavigationType)type
+{
+ // Force clicked links to open in Safari, not in this window.
+ if (type == UIWebViewNavigationTypeLinkClicked) {
+ [[UIApplication sharedApplication] openURL:[req URL]];
+ return NO;
+ }
+ return YES;
+}
+
+
+- (void) setFrame: (CGRect)r
+{
+ [super setFrame: r];
+ r.origin.x = 0;
+ r.origin.y = 0;
+ [webView setFrame: r];
+}
+
+
+- (NSString *) stripTags:(NSString *)str
+{
+ NSString *result = @"";
+
+ // Add newlines.
+ str = [str stringByReplacingOccurrencesOfString:@"
"
+ withString:@"
"
+ options:NSCaseInsensitiveSearch
+ range:NSMakeRange(0, [str length])];
+ str = [str stringByReplacingOccurrencesOfString:@"
"
+ withString:@"\n"
+ options:NSCaseInsensitiveSearch
+ range:NSMakeRange(0, [str length])];
+
+ // Remove HREFs.
+ for (NSString *s in [str componentsSeparatedByString: @"<"]) {
+ NSRange r = [s rangeOfString:@">"];
+ if (r.length > 0)
+ s = [s substringFromIndex: r.location + r.length];
+ result = [result stringByAppendingString: s];
+ }
+
+ // Compress internal horizontal whitespace.
+ str = result;
+ result = @"";
+ for (NSString *s in [str componentsSeparatedByCharactersInSet:
+ [NSCharacterSet whitespaceCharacterSet]]) {
+ if ([result length] == 0)
+ result = s;
+ else if ([s length] > 0)
+ result = [NSString stringWithFormat: @"%@ %@", result, s];
+ }
+
+ return result;
+}
+
+
+- (void) sizeToFit
+{
+ CGRect r = [self frame];
+
+ /* It would be sensible to just ask the UIWebView how tall the page is,
+ instead of hoping that NSString and UIWebView measure fonts and do
+ wrapping in exactly the same way, but since UIWebView is asynchronous,
+ we'd have to wait for the document to load first, e.g.:
+
+ - Start the document loading;
+ - return a default height to use for the UITableViewCell;
+ - wait for the webViewDidFinishLoad delegate method to fire;
+ - then force the UITableView to reload, to pick up the new height.
+
+ But I couldn't make that work.
+ */
+# if 0
+ r.size.height = [[webView
+ stringByEvaluatingJavaScriptFromString:
+ @"document.body.offsetHeight"]
+ doubleValue];
+# else
+ NSString *text = [self stripTags: html];
+ CGSize s = r.size;
+ s.height = 999999;
+ s = [text boundingRectWithSize:s
+ options:NSStringDrawingUsesLineFragmentOrigin
+ attributes:@{NSFontAttributeName: font}
+ context:nil].size;
+ r.size.height = s.height;
+# endif
+
+ [self setFrame: r];
+}
+
+
+- (void) dealloc
+{
+ [html release];
+ [font release];
+ [webView release];
+ [super dealloc];
+}
+
+@end
+
+#endif // USE_IPHONE && USE_HTML_LABELS
+
+
+@interface XScreenSaverConfigSheet (Private)
+
+- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent;
+
+# ifndef USE_IPHONE
+- (void) placeChild: (NSView *)c on:(NSView *)p right:(BOOL)r;
+- (void) placeChild: (NSView *)c on:(NSView *)p;
+static NSView *last_child (NSView *parent);
+static void layout_group (NSView *group, BOOL horiz_p);
+# else // USE_IPHONE
+- (void) placeChild: (NSObject *)c on:(NSView *)p right:(BOOL)r;
+- (void) placeChild: (NSObject *)c on:(NSView *)p;
+- (void) placeSeparator;
+- (void) bindResource:(NSObject *)ctl key:(NSString *)k reload:(BOOL)r;
+- (void) refreshTableView;
+# endif // USE_IPHONE
+
+@end
+
+
+@implementation XScreenSaverConfigSheet
+
+# define LEFT_MARGIN 20 // left edge of window
+# define COLUMN_SPACING 10 // gap between e.g. labels and text fields
+# define LEFT_LABEL_WIDTH 70 // width of all left labels
+# define LINE_SPACING 10 // leading between each line
+
+# define FONT_SIZE 17 // Magic hardcoded UITableView font size.
+
+#pragma mark Talking to the resource database
+
+
+/* Normally we read resources by looking up "KEY" in the database
+ "org.jwz.xscreensaver.SAVERNAME". But in the all-in-one iPhone
+ app, everything is stored in the database "org.jwz.xscreensaver"
+ instead, so transform keys to "SAVERNAME.KEY".
+
+ NOTE: This is duplicated in PrefsReader.m, cause I suck.
+ */
+- (NSString *) makeKey:(NSString *)key
+{
+# ifdef USE_IPHONE
+ NSString *prefix = [saver_name stringByAppendingString:@"."];
+ if (! [key hasPrefix:prefix]) // Don't double up!
+ key = [prefix stringByAppendingString:key];
+# endif
+ return key;
+}
+
+
+- (NSString *) makeCKey:(const char *)key
+{
+ return [self makeKey:[NSString stringWithCString:key
+ encoding:NSUTF8StringEncoding]];
+}
+
+
+/* Given a command-line option, returns the corresponding resource name.
+ Any arguments in the switch string are ignored (e.g., "-foo x").
+ */
+- (NSString *) switchToResource:(NSString *)cmdline_switch
+ opts:(const XrmOptionDescRec *)opts_array
+ valRet:(NSString **)val_ret
+{
+ char buf[1280];
+ char *tail = 0;
+ NSAssert(cmdline_switch, @"cmdline switch is null");
+ if (! [cmdline_switch getCString:buf maxLength:sizeof(buf)
+ encoding:NSUTF8StringEncoding]) {
+ NSAssert1(0, @"unable to convert %@", cmdline_switch);
+ return 0;
+ }
+ char *s = strpbrk(buf, " \t\r\n");
+ if (s && *s) {
+ *s = 0;
+ tail = s+1;
+ while (*tail && (*tail == ' ' || *tail == '\t'))
+ tail++;
+ }
+
+ while (opts_array[0].option) {
+ if (!strcmp (opts_array[0].option, buf)) {
+ const char *ret = 0;
+
+ if (opts_array[0].argKind == XrmoptionNoArg) {
+ if (tail && *tail)
+ NSAssert1 (0, @"expected no args to switch: \"%@\"",
+ cmdline_switch);
+ ret = opts_array[0].value;
+ } else {
+ if (!tail || !*tail)
+ NSAssert1 (0, @"expected args to switch: \"%@\"",
+ cmdline_switch);
+ ret = tail;
+ }
+
+ if (val_ret)
+ *val_ret = (ret
+ ? [NSString stringWithCString:ret
+ encoding:NSUTF8StringEncoding]
+ : 0);
+
+ const char *res = opts_array[0].specifier;
+ while (*res && (*res == '.' || *res == '*'))
+ res++;
+ return [self makeCKey:res];
+ }
+ opts_array++;
+ }
+
+ NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch);
+ return 0;
+}
+
+
+- (NSUserDefaultsController *)controllerForKey:(NSString *)key
+{
+ static NSDictionary *a = 0;
+ if (! a) {
+ a = UPDATER_DEFAULTS;
+ [a retain];
+ }
+ if ([a objectForKey:key])
+ // These preferences are global to all xscreensavers.
+ return globalDefaultsController;
+ else
+ // All other preferences are per-saver.
+ return userDefaultsController;
+}
+
+
+#ifdef USE_IPHONE
+
+// Called when a slider is bonked.
+//
+- (void)sliderAction:(UISlider*)sender
+{
+ if ([active_text_field canResignFirstResponder])
+ [active_text_field resignFirstResponder];
+ NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
+
+ // Hacky API. See comment in InvertedSlider.m.
+ double v = ([sender isKindOfClass: [InvertedSlider class]]
+ ? [(InvertedSlider *) sender transformedValue]
+ : [sender value]);
+
+ [[self controllerForKey:pref_key]
+ setObject:((v == (int) v)
+ ? [NSNumber numberWithInt:(int) v]
+ : [NSNumber numberWithDouble: v])
+ forKey:pref_key];
+}
+
+// Called when a checkbox/switch is bonked.
+//
+- (void)switchAction:(UISwitch*)sender
+{
+ if ([active_text_field canResignFirstResponder])
+ [active_text_field resignFirstResponder];
+ NSString *pref_key = [pref_keys objectAtIndex: [sender tag]];
+ NSString *v = ([sender isOn] ? @"true" : @"false");
+ [[self controllerForKey:pref_key] setObject:v forKey:pref_key];
+}
+
+# ifdef USE_PICKER_VIEW
+// Called when a picker is bonked.
+//
+- (void)pickerView:(UIPickerView *)pv
+ didSelectRow:(NSInteger)row
+ inComponent:(NSInteger)column
+{
+ if ([active_text_field canResignFirstResponder])
+ [active_text_field resignFirstResponder];
+
+ NSAssert (column == 0, @"internal error");
+ NSArray *a = [picker_values objectAtIndex: [pv tag]];
+ if (! a) return; // Too early?
+ a = [a objectAtIndex:row];
+ NSAssert (a, @"missing row");
+
+//NSString *label = [a objectAtIndex:0];
+ NSString *pref_key = [a objectAtIndex:1];
+ NSObject *pref_val = [a objectAtIndex:2];
+ [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
+}
+# else // !USE_PICKER_VIEW
+
+// Called when a RadioButton is bonked.
+//
+- (void)radioAction:(RadioButton*)sender
+{
+ if ([active_text_field canResignFirstResponder])
+ [active_text_field resignFirstResponder];
+
+ NSArray *item = [[sender items] objectAtIndex: [sender index]];
+ NSString *pref_key = [item objectAtIndex:1];
+ NSObject *pref_val = [item objectAtIndex:2];
+ [[self controllerForKey:pref_key] setObject:pref_val forKey:pref_key];
+}
+
+- (BOOL)textFieldShouldBeginEditing:(UITextField *)tf
+{
+ active_text_field = tf;
+ return YES;
+}
+
+- (void)textFieldDidEndEditing:(UITextField *)tf
+{
+ NSString *pref_key = [pref_keys objectAtIndex: [tf tag]];
+ NSString *txt = [tf text];
+ [[self controllerForKey:pref_key] setObject:txt forKey:pref_key];
+}
+
+- (BOOL)textFieldShouldReturn:(UITextField *)tf
+{
+ active_text_field = nil;
+ [tf resignFirstResponder];
+ return YES;
+}
+
+# endif // !USE_PICKER_VIEW
+
+#endif // USE_IPHONE
+
+
+# ifndef USE_IPHONE
+
+- (void) okAction:(NSObject *)arg
+{
+ // Without the setAppliesImmediately:, when the saver restarts, it's still
+ // got the old settings. -[XScreenSaverConfigSheet traverseTree] sets this
+ // to NO; default is YES.
+
+ // #### However: I'm told that when these are set to YES, then changes to
+ // 'textLiteral', 'textURL' and 'textProgram' are ignored, but 'textFile'
+ // works. In StarWars, at least...
+
+ [userDefaultsController setAppliesImmediately:YES];
+ [globalDefaultsController setAppliesImmediately:YES];
+ [userDefaultsController commitEditing];
+ [globalDefaultsController commitEditing];
+ [userDefaultsController save:self];
+ [globalDefaultsController save:self];
+ [NSApp endSheet:self returnCode:NSOKButton];
+ [self close];
+}
+
+- (void) cancelAction:(NSObject *)arg
+{
+ [userDefaultsController revert:self];
+ [globalDefaultsController revert:self];
+ [NSApp endSheet:self returnCode:NSCancelButton];
+ [self close];
+}
+# endif // !USE_IPHONE
+
+
+- (void) resetAction:(NSObject *)arg
+{
+# ifndef USE_IPHONE
+ [userDefaultsController revertToInitialValues:self];
+ [globalDefaultsController revertToInitialValues:self];
+# else // USE_IPHONE
+
+ for (NSString *key in defaultOptions) {
+ NSObject *val = [defaultOptions objectForKey:key];
+ [[self controllerForKey:key] setObject:val forKey:key];
+ }
+
+ for (UIControl *ctl in pref_ctls) {
+ NSString *pref_key = [pref_keys objectAtIndex: ctl.tag];
+ [self bindResource:ctl key:pref_key reload:YES];
+ }
+
+ [self refreshTableView];
+# endif // USE_IPHONE
+}
+
+
+/* Connects a control (checkbox, etc) to the corresponding preferences key.
+ */
+- (void) bindResource:(NSObject *)control key:(NSString *)pref_key
+ reload:(BOOL)reload_p
+{
+ NSUserDefaultsController *prefs = [self controllerForKey:pref_key];
+# ifndef USE_IPHONE
+ NSDictionary *opts_dict = nil;
+ NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]]
+ ? @"selectedObject"
+ : ([control isKindOfClass:[NSMatrix class]]
+ ? @"selectedIndex"
+ : @"value"));
+
+ if ([control isKindOfClass:[NSMatrix class]]) {
+ opts_dict = @{ NSValueTransformerNameBindingOption:
+ @"TextModeTransformer" };
+ }
+
+ [control bind:bindto
+ toObject:prefs
+ withKeyPath:[@"values." stringByAppendingString: pref_key]
+ options:opts_dict];
+
+# else // USE_IPHONE
+ SEL sel;
+ NSObject *val = [prefs objectForKey:pref_key];
+ NSString *sval = 0;
+ double dval = 0;
+
+ if ([val isKindOfClass:[NSString class]]) {
+ sval = (NSString *) val;
+ if (NSOrderedSame == [sval caseInsensitiveCompare:@"true"] ||
+ NSOrderedSame == [sval caseInsensitiveCompare:@"yes"] ||
+ NSOrderedSame == [sval caseInsensitiveCompare:@"1"])
+ dval = 1;
+ else
+ dval = [sval doubleValue];
+ } else if ([val isKindOfClass:[NSNumber class]]) {
+ // NSBoolean (__NSCFBoolean) is really NSNumber.
+ dval = [(NSNumber *) val doubleValue];
+ sval = [(NSNumber *) val stringValue];
+ }
+
+ if ([control isKindOfClass:[UISlider class]]) {
+ sel = @selector(sliderAction:);
+ // Hacky API. See comment in InvertedSlider.m.
+ if ([control isKindOfClass:[InvertedSlider class]])
+ [(InvertedSlider *) control setTransformedValue: dval];
+ else
+ [(UISlider *) control setValue: dval];
+ } else if ([control isKindOfClass:[UISwitch class]]) {
+ sel = @selector(switchAction:);
+ [(UISwitch *) control setOn: ((int) dval != 0)];
+# ifdef USE_PICKER_VIEW
+ } else if ([control isKindOfClass:[UIPickerView class]]) {
+ sel = 0;
+ [(UIPickerView *) control selectRow:((int)dval) inComponent:0
+ animated:NO];
+# else // !USE_PICKER_VIEW
+ } else if ([control isKindOfClass:[RadioButton class]]) {
+ sel = 0; // radioAction: sent from didSelectRowAtIndexPath.
+ } else if ([control isKindOfClass:[UITextField class]]) {
+ sel = 0; // ####
+ [(UITextField *) control setText: sval];
+# endif // !USE_PICKER_VIEW
+ } else {
+ NSAssert (0, @"unknown class");
+ }
+
+ // NSLog(@"\"%@\" = \"%@\" [%@, %.1f]", pref_key, val, [val class], dval);
+
+ if (!reload_p) {
+ if (! pref_keys) {
+ pref_keys = [[NSMutableArray arrayWithCapacity:10] retain];
+ pref_ctls = [[NSMutableArray arrayWithCapacity:10] retain];
+ }
+
+ [pref_keys addObject: [self makeKey:pref_key]];
+ [pref_ctls addObject: control];
+ ((UIControl *) control).tag = [pref_keys count] - 1;
+
+ if (sel) {
+ [(UIControl *) control addTarget:self action:sel
+ forControlEvents:UIControlEventValueChanged];
+ }
+ }
+
+# endif // USE_IPHONE
+
+# if 0
+ NSObject *def = [[prefs defaults] objectForKey:pref_key];
+ NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key];
+ s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0];
+ s = [NSString stringWithFormat:@"%@ = %@", s,
+ ([def isKindOfClass:[NSString class]]
+ ? [NSString stringWithFormat:@"\"%@\"", def]
+ : def)];
+ s = [s stringByPaddingToLength:30 withString:@" " startingAtIndex:0];
+ s = [NSString stringWithFormat:@"%@ %@ / %@", s,
+ [def class], [control class]];
+# ifndef USE_IPHONE
+ s = [NSString stringWithFormat:@"%@ / %@", s, bindto];
+# endif
+ NSLog (@"%@", s);
+# endif
+}
+
+
+- (void) bindResource:(NSObject *)control key:(NSString *)pref_key
+{
+ [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO];
+}
+
+
+
+- (void) bindSwitch:(NSObject *)control
+ cmdline:(NSString *)cmd
+{
+ [self bindResource:control
+ key:[self switchToResource:cmd opts:opts valRet:0]];
+}
+
+
+#pragma mark Text-manipulating utilities
+
+
+static NSString *
+unwrap (NSString *text)
+{
+ // Unwrap lines: delete \n but do not delete \n\n.
+ //
+ NSArray *lines = [text componentsSeparatedByString:@"\n"];
+ NSUInteger i, nlines = [lines count];
+ BOOL eolp = YES;
+
+ text = @"\n"; // start with one blank line
+
+ // skip trailing blank lines in file
+ for (i = nlines-1; i > 0; i--) {
+ NSString *s = (NSString *) [lines objectAtIndex:i];
+ if ([s length] > 0)
+ break;
+ nlines--;
+ }
+
+ // skip leading blank lines in file
+ for (i = 0; i < nlines; i++) {
+ NSString *s = (NSString *) [lines objectAtIndex:i];
+ if ([s length] > 0)
+ break;
+ }
+
+ // unwrap
+ Bool any = NO;
+ for (; i < nlines; i++) {
+ NSString *s = (NSString *) [lines objectAtIndex:i];
+ if ([s length] == 0) {
+ text = [text stringByAppendingString:@"\n\n"];
+ eolp = YES;
+ } else if ([s characterAtIndex:0] == ' ' ||
+ [s hasPrefix:@"Copyright "] ||
+ [s hasPrefix:@"https://"] ||
+ [s hasPrefix:@"http://"]) {
+ // don't unwrap if the following line begins with whitespace,
+ // or with the word "Copyright", or if it begins with a URL.
+ if (any && !eolp)
+ text = [text stringByAppendingString:@"\n"];
+ text = [text stringByAppendingString:s];
+ any = YES;
+ eolp = NO;
+ } else {
+ if (!eolp)
+ text = [text stringByAppendingString:@" "];
+ text = [text stringByAppendingString:s];
+ eolp = NO;
+ any = YES;
+ }
+ }
+
+ return text;
+}
+
+
+# ifndef USE_IPHONE
+/* Makes the text up to the first comma be bold.
+ */
+static void
+boldify (NSText *nstext)
+{
+ NSString *text = [nstext string];
+ NSRange r = [text rangeOfString:@"," options:0];
+ r.length = r.location+1;
+
+ r.location = 0;
+
+ NSFont *font = [nstext font];
+ font = [NSFont boldSystemFontOfSize:[font pointSize]];
+ [nstext setFont:font range:r];
+}
+# endif // !USE_IPHONE
+
+
+/* Creates a human-readable anchor to put on a URL.
+ */
+static char *
+anchorize (const char *url)
+{
+ const char *wiki1 = "http://en.wikipedia.org/wiki/";
+ const char *wiki2 = "https://en.wikipedia.org/wiki/";
+ const char *math1 = "http://mathworld.wolfram.com/";
+ const char *math2 = "https://mathworld.wolfram.com/";
+ if (!strncmp (wiki1, url, strlen(wiki1)) ||
+ !strncmp (wiki2, url, strlen(wiki2))) {
+ char *anchor = (char *) malloc (strlen(url) * 3 + 10);
+ strcpy (anchor, "Wikipedia: \"");
+ const char *in = url + strlen(!strncmp (wiki1, url, strlen(wiki1))
+ ? wiki1 : wiki2);
+ char *out = anchor + strlen(anchor);
+ while (*in) {
+ if (*in == '_') {
+ *out++ = ' ';
+ } else if (*in == '#') {
+ *out++ = ':';
+ *out++ = ' ';
+ } else if (*in == '%') {
+ char hex[3];
+ hex[0] = in[1];
+ hex[1] = in[2];
+ hex[2] = 0;
+ int n = 0;
+ sscanf (hex, "%x", &n);
+ *out++ = (char) n;
+ in += 2;
+ } else {
+ *out++ = *in;
+ }
+ in++;
+ }
+ *out++ = '"';
+ *out = 0;
+ return anchor;
+
+ } else if (!strncmp (math1, url, strlen(math1)) ||
+ !strncmp (math2, url, strlen(math2))) {
+ char *anchor = (char *) malloc (strlen(url) * 3 + 10);
+ strcpy (anchor, "MathWorld: \"");
+ const char *start = url + strlen(!strncmp (math1, url, strlen(math1))
+ ? math1 : math2);
+ const char *in = start;
+ char *out = anchor + strlen(anchor);
+ while (*in) {
+ if (*in == '_') {
+ *out++ = ' ';
+ } else if (in != start && *in >= 'A' && *in <= 'Z') {
+ *out++ = ' ';
+ *out++ = *in;
+ } else if (!strncmp (in, ".htm", 4)) {
+ break;
+ } else {
+ *out++ = *in;
+ }
+ in++;
+ }
+ *out++ = '"';
+ *out = 0;
+ return anchor;
+
+ } else {
+ return strdup (url);
+ }
+}
+
+
+#if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS)
+
+/* Converts any http: URLs in the given text field to clickable links.
+ */
+static void
+hreffify (NSText *nstext)
+{
+# ifndef USE_IPHONE
+ NSString *text = [nstext string];
+ [nstext setRichText:YES];
+# else
+ NSString *text = [nstext text];
+# endif
+
+ NSUInteger L = [text length];
+ NSRange start; // range is start-of-search to end-of-string
+ start.location = 0;
+ start.length = L;
+ while (start.location < L) {
+
+ // Find the beginning of a URL...
+ //
+ NSRange r2 = [text rangeOfString: @"http://" options:0 range:start];
+ NSRange r3 = [text rangeOfString:@"https://" options:0 range:start];
+ if ((r2.location == NSNotFound &&
+ r3.location != NSNotFound) ||
+ (r2.location != NSNotFound &&
+ r3.location != NSNotFound &&
+ r3.location < r2.location))
+ r2 = r3;
+ if (r2.location == NSNotFound)
+ break;
+
+ // Next time around, start searching after this.
+ start.location = r2.location + r2.length;
+ start.length = L - start.location;
+
+ // Find the end of a URL (whitespace or EOF)...
+ //
+ r3 = [text rangeOfCharacterFromSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]
+ options:0 range:start];
+ if (r3.location == NSNotFound) // EOF
+ r3.location = L, r3.length = 0;
+
+ // Next time around, start searching after this.
+ start.location = r3.location;
+ start.length = L - start.location;
+
+ // Set r2 to the start/length of this URL.
+ r2.length = start.location - r2.location;
+
+ // Extract the URL.
+ NSString *nsurl = [text substringWithRange:r2];
+ const char *url = [nsurl UTF8String];
+
+ // If this is a Wikipedia URL, make the linked text be prettier.
+ //
+ char *anchor = anchorize(url);
+
+# ifndef USE_IPHONE
+
+ // Construct the RTF corresponding to anchor
+ //
+ const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}";
+ char *rtf = malloc (strlen (fmt) + strlen(url) + strlen(anchor) + 10);
+ sprintf (rtf, fmt, url, anchor);
+
+ NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)];
+ [nstext replaceCharactersInRange:r2 withRTF:rtfdata];
+
+# else // !USE_IPHONE
+ // *anchor = 0; // Omit Wikipedia anchor
+ text = [text stringByReplacingCharactersInRange:r2
+ withString:[NSString stringWithCString:anchor
+ encoding:NSUTF8StringEncoding]];
+ // text = [text stringByReplacingOccurrencesOfString:@"\n\n\n"
+ // withString:@"\n\n"];
+# endif // !USE_IPHONE
+
+ free (anchor);
+
+ NSUInteger L2 = [text length]; // might have changed
+ start.location -= (L - L2);
+ L = L2;
+ }
+
+# ifdef USE_IPHONE
+ [nstext setText:text];
+ [nstext sizeToFit];
+# endif
+}
+
+#endif /* !USE_IPHONE || !USE_HTML_LABELS */
+
+
+
+#pragma mark Creating controls from XML
+
+
+/* Parse the attributes of an XML tag into a dictionary.
+ For input, the dictionary should have as attributes the keys, each
+ with @"" as their value.
+ On output, the dictionary will set the keys to the values specified,
+ and keys that were not specified will not be present in the dictionary.
+ Warnings are printed if there are duplicate or unknown attributes.
+ */
+- (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node
+{
+ NSArray *attrs = [(NSXMLElement *) node attributes];
+ NSUInteger n = [attrs count];
+ int i;
+
+ // For each key in the dictionary, fill in the dict with the corresponding
+ // value. The value @"" is assumed to mean "un-set". Issue a warning if
+ // an attribute is specified twice.
+ //
+ for (i = 0; i < n; i++) {
+ NSXMLNode *attr = [attrs objectAtIndex:i];
+ NSString *key = [attr name];
+ NSString *val = [attr objectValue];
+ NSString *old = [dict objectForKey:key];
+
+ if (! old) {
+ NSAssert2 (0, @"unknown attribute \"%@\" in \"%@\"", key, [node name]);
+ } else if ([old length] != 0) {
+ NSAssert3 (0, @"duplicate %@: \"%@\", \"%@\"", key, old, val);
+ } else {
+ [dict setValue:val forKey:key];
+ }
+ }
+
+ // Remove from the dictionary any keys whose value is still @"",
+ // meaning there was no such attribute specified.
+ //
+ NSArray *keys = [dict allKeys];
+ n = [keys count];
+ for (i = 0; i < n; i++) {
+ NSString *key = [keys objectAtIndex:i];
+ NSString *val = [dict objectForKey:key];
+ if ([val length] == 0)
+ [dict removeObjectForKey:key];
+ }
+
+# ifdef USE_IPHONE
+ // Kludge for starwars.xml:
+ // If there is a "_low-label" and no "_label", but "_low-label" contains
+ // spaces, divide them.
+ NSString *lab = [dict objectForKey:@"_label"];
+ NSString *low = [dict objectForKey:@"_low-label"];
+ if (low && !lab) {
+ NSArray *split =
+ [[[low stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]
+ componentsSeparatedByString: @" "]
+ filteredArrayUsingPredicate:
+ [NSPredicate predicateWithFormat:@"length > 0"]];
+ if (split && [split count] == 2) {
+ [dict setValue:[split objectAtIndex:0] forKey:@"_label"];
+ [dict setValue:[split objectAtIndex:1] forKey:@"_low-label"];
+ }
+ }
+# endif // USE_IPHONE
+}
+
+
+/* Handle the options on the top level tag.
+ */
+- (NSString *) parseXScreenSaverTag:(NSXMLNode *)node
+{
+ NSMutableDictionary *dict = [@{ @"name": @"",
+ @"_label": @"",
+ @"gl": @"" }
+ mutableCopy];
+ [self parseAttrs:dict node:node];
+ NSString *name = [dict objectForKey:@"name"];
+ NSString *label = [dict objectForKey:@"_label"];
+ [dict release];
+ dict = 0;
+
+ NSAssert1 (label, @"no _label in %@", [node name]);
+ NSAssert1 (name, @"no name in \"%@\"", label);
+ return label;
+}
+
+
+/* Creates a label: an un-editable NSTextField displaying the given text.
+ */
+- (LABEL *) makeLabel:(NSString *)text
+{
+ NSRect rect;
+ rect.origin.x = rect.origin.y = 0;
+ rect.size.width = rect.size.height = 10;
+# ifndef USE_IPHONE
+ NSTextField *lab = [[NSTextField alloc] initWithFrame:rect];
+ [lab setSelectable:NO];
+ [lab setEditable:NO];
+ [lab setBezeled:NO];
+ [lab setDrawsBackground:NO];
+ [lab setStringValue:text];
+ [lab sizeToFit];
+# else // USE_IPHONE
+ UILabel *lab = [[UILabel alloc] initWithFrame:rect];
+ [lab setText: [text stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
+ [lab setBackgroundColor:[UIColor clearColor]];
+ [lab setNumberOfLines:0]; // unlimited
+ // [lab setLineBreakMode:UILineBreakModeWordWrap];
+ [lab setLineBreakMode:NSLineBreakByTruncatingHead];
+ [lab setAutoresizingMask: (UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight)];
+# endif // USE_IPHONE
+ [lab autorelease];
+ return lab;
+}
+
+
+/* Creates the checkbox (NSButton) described by the given XML node.
+ */
+- (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent
+{
+ NSMutableDictionary *dict = [@{ @"id": @"",
+ @"_label": @"",
+ @"arg-set": @"",
+ @"arg-unset": @"" }
+ mutableCopy];
+ [self parseAttrs:dict node:node];
+ NSString *label = [dict objectForKey:@"_label"];
+ NSString *arg_set = [dict objectForKey:@"arg-set"];
+ NSString *arg_unset = [dict objectForKey:@"arg-unset"];
+ [dict release];
+ dict = 0;
+
+ if (!label) {
+ NSAssert1 (0, @"no _label in %@", [node name]);
+ return;
+ }
+ if (!arg_set && !arg_unset) {
+ NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"",
+ label);
+ }
+ if (arg_set && arg_unset) {
+ NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"",
+ label);
+ }
+
+ // sanity-check the choice of argument names.
+ //
+ if (arg_set && ([arg_set hasPrefix:@"-no-"] ||
+ [arg_set hasPrefix:@"--no-"]))
+ NSLog (@"arg-set should not be a \"no\" option in \"%@\": %@",
+ label, arg_set);
+ if (arg_unset && (![arg_unset hasPrefix:@"-no-"] &&
+ ![arg_unset hasPrefix:@"--no-"]))
+ NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@",
+ label, arg_unset);
+
+ NSRect rect;
+ rect.origin.x = rect.origin.y = 0;
+ rect.size.width = rect.size.height = 10;
+
+# ifndef USE_IPHONE
+
+ NSButton *button = [[NSButton alloc] initWithFrame:rect];
+ [button setButtonType:NSSwitchButton];
+ [button setTitle:label];
+ [button sizeToFit];
+ [self placeChild:button on:parent];
+
+# else // USE_IPHONE
+
+ LABEL *lab = [self makeLabel:label];
+ [self placeChild:lab on:parent];
+ UISwitch *button = [[UISwitch alloc] initWithFrame:rect];
+ [self placeChild:button on:parent right:YES];
+
+# endif // USE_IPHONE
+
+ [self bindSwitch:button cmdline:(arg_set ? arg_set : arg_unset)];
+ [button release];
+}
+
+
+/* Creates the number selection control described by the given XML node.
+ If "type=slider", it's an NSSlider.
+ If "type=spinbutton", it's a text field with up/down arrows next to it.
+ */
+- (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent
+{
+ NSMutableDictionary *dict = [@{ @"id": @"",
+ @"_label": @"",
+ @"_low-label": @"",
+ @"_high-label": @"",
+ @"type": @"",
+ @"arg": @"",
+ @"low": @"",
+ @"high": @"",
+ @"default": @"",
+ @"convert": @"" }
+ mutableCopy];
+ [self parseAttrs:dict node:node];
+ NSString *label = [dict objectForKey:@"_label"];
+ NSString *low_label = [dict objectForKey:@"_low-label"];
+ NSString *high_label = [dict objectForKey:@"_high-label"];
+ NSString *type = [dict objectForKey:@"type"];
+ NSString *arg = [dict objectForKey:@"arg"];
+ NSString *low = [dict objectForKey:@"low"];
+ NSString *high = [dict objectForKey:@"high"];
+ NSString *def = [dict objectForKey:@"default"];
+ NSString *cvt = [dict objectForKey:@"convert"];
+ [dict release];
+ dict = 0;
+
+ NSAssert1 (arg, @"no arg in %@", label);
+ NSAssert1 (type, @"no type in %@", label);
+
+ if (! low) {
+ NSAssert1 (0, @"no low in %@", [node name]);
+ return;
+ }
+ if (! high) {
+ NSAssert1 (0, @"no high in %@", [node name]);
+ return;
+ }
+ if (! def) {
+ NSAssert1 (0, @"no default in %@", [node name]);
+ return;
+ }
+ if (cvt && ![cvt isEqualToString:@"invert"]) {
+ NSAssert1 (0, @"if provided, \"convert\" must be \"invert\" in %@",
+ label);
+ }
+
+ // If either the min or max field contains a decimal point, then this
+ // option may have a floating point value; otherwise, it is constrained
+ // to be an integer.
+ //
+ NSCharacterSet *dot =
+ [NSCharacterSet characterSetWithCharactersInString:@"."];
+ BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound ||
+ [high rangeOfCharacterFromSet:dot].location != NSNotFound);
+
+ if ([type isEqualToString:@"slider"]
+# ifdef USE_IPHONE // On iPhone, we use sliders for all numeric values.
+ || [type isEqualToString:@"spinbutton"]
+# endif
+ ) {
+
+ NSRect rect;
+ rect.origin.x = rect.origin.y = 0;
+ rect.size.width = 150;
+ rect.size.height = 23; // apparent min height for slider with ticks...
+ NSSlider *slider;
+ slider = [[InvertedSlider alloc] initWithFrame:rect
+ inverted: !!cvt
+ integers: !float_p];
+ [slider setMaxValue:[high doubleValue]];
+ [slider setMinValue:[low doubleValue]];
+
+ int range = [slider maxValue] - [slider minValue] + 1;
+ int range2 = range;
+ int max_ticks = 21;
+ while (range2 > max_ticks)
+ range2 /= 10;
+
+# ifndef USE_IPHONE
+ // If we have elided ticks, leave it at the max number of ticks.
+ if (range != range2 && range2 < max_ticks)
+ range2 = max_ticks;
+
+ // If it's a float, always display the max number of ticks.
+ if (float_p && range2 < max_ticks)
+ range2 = max_ticks;
+
+ [slider setNumberOfTickMarks:range2];
+
+ [slider setAllowsTickMarkValuesOnly:
+ (range == range2 && // we are showing the actual number of ticks
+ !float_p)]; // and we want integer results
+# endif // !USE_IPHONE
+
+ // #### Note: when the slider's range is large enough that we aren't
+ // showing all possible ticks, the slider's value is not constrained
+ // to be an integer, even though it should be...
+ // Maybe we need to use a value converter or something?
+
+ LABEL *lab;
+ if (label) {
+ lab = [self makeLabel:label];
+ [self placeChild:lab on:parent];
+# ifdef USE_IPHONE
+ if (low_label) {
+ CGFloat s = [NSFont systemFontSize] + 4;
+ [lab setFont:[NSFont boldSystemFontOfSize:s]];
+ }
+# endif
+ }
+
+ if (low_label) {
+ lab = [self makeLabel:low_label];
+ [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+# ifndef USE_IPHONE
+ [lab setAlignment:1]; // right aligned
+ rect = [lab frame];
+ if (rect.size.width < LEFT_LABEL_WIDTH)
+ rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
+ rect.size.height = [slider frame].size.height;
+ [lab setFrame:rect];
+ [self placeChild:lab on:parent];
+# else // USE_IPHONE
+ [lab setTextAlignment: NSTextAlignmentRight];
+ // Sometimes rotation screws up truncation.
+ [lab setLineBreakMode:NSLineBreakByClipping];
+ [self placeChild:lab on:parent right:(label ? YES : NO)];
+# endif // USE_IPHONE
+ }
+
+# ifndef USE_IPHONE
+ [self placeChild:slider on:parent right:(low_label ? YES : NO)];
+# else // USE_IPHONE
+ [self placeChild:slider on:parent right:(label || low_label ? YES : NO)];
+# endif // USE_IPHONE
+
+ if (low_label) {
+ // Make left label be same height as slider.
+ rect = [lab frame];
+ rect.size.height = [slider frame].size.height;
+ [lab setFrame:rect];
+ }
+
+ if (! low_label) {
+ rect = [slider frame];
+ if (rect.origin.x < LEFT_LABEL_WIDTH)
+ rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled sliders line up too
+ [slider setFrame:rect];
+ }
+
+ if (high_label) {
+ lab = [self makeLabel:high_label];
+ [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ rect = [lab frame];
+
+ // Make right label be same height as slider.
+ rect.size.height = [slider frame].size.height;
+ [lab setFrame:rect];
+# ifdef USE_IPHONE
+ // Sometimes rotation screws up truncation.
+ [lab setLineBreakMode:NSLineBreakByClipping];
+# endif
+ [self placeChild:lab on:parent right:YES];
+ }
+
+ [self bindSwitch:slider cmdline:arg];
+ [slider release];
+
+#ifndef USE_IPHONE // On iPhone, we use sliders for all numeric values.
+
+ } else if ([type isEqualToString:@"spinbutton"]) {
+
+ if (! label) {
+ NSAssert1 (0, @"no _label in spinbutton %@", [node name]);
+ return;
+ }
+ NSAssert1 (!low_label,
+ @"low-label not allowed in spinbutton \"%@\"", [node name]);
+ NSAssert1 (!high_label,
+ @"high-label not allowed in spinbutton \"%@\"", [node name]);
+ NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"",
+ [node name]);
+
+ NSRect rect;
+ rect.origin.x = rect.origin.y = 0;
+ rect.size.width = rect.size.height = 10;
+
+ NSTextField *txt = [[NSTextField alloc] initWithFrame:rect];
+ [txt setStringValue:@"0000.0"];
+ [txt sizeToFit];
+ [txt setStringValue:@""];
+
+ if (label) {
+ LABEL *lab = [self makeLabel:label];
+ //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [lab setAlignment:1]; // right aligned
+ rect = [lab frame];
+ if (rect.size.width < LEFT_LABEL_WIDTH)
+ rect.size.width = LEFT_LABEL_WIDTH; // make all left labels same size
+ rect.size.height = [txt frame].size.height;
+ [lab setFrame:rect];
+ [self placeChild:lab on:parent];
+ }
+
+ [self placeChild:txt on:parent right:(label ? YES : NO)];
+
+ if (! label) {
+ rect = [txt frame];
+ if (rect.origin.x < LEFT_LABEL_WIDTH)
+ rect.origin.x = LEFT_LABEL_WIDTH; // make unlabelled spinbtns line up
+ [txt setFrame:rect];
+ }
+
+ rect.size.width = rect.size.height = 10;
+ NSStepper *step = [[NSStepper alloc] initWithFrame:rect];
+ [step sizeToFit];
+ [self placeChild:step on:parent right:YES];
+ rect = [step frame];
+ rect.origin.x -= COLUMN_SPACING; // this one goes close
+ rect.origin.y += ([txt frame].size.height - rect.size.height) / 2;
+ [step setFrame:rect];
+
+ [step setMinValue:[low doubleValue]];
+ [step setMaxValue:[high doubleValue]];
+ [step setAutorepeat:YES];
+ [step setValueWraps:NO];
+
+ double range = [high doubleValue] - [low doubleValue];
+ if (range < 1.0)
+ [step setIncrement:range / 10.0];
+ else if (range >= 500)
+ [step setIncrement:range / 100.0];
+ else
+ [step setIncrement:1.0];
+
+ NSNumberFormatter *fmt = [[[NSNumberFormatter alloc] init] autorelease];
+ [fmt setFormatterBehavior:NSNumberFormatterBehavior10_4];
+ [fmt setNumberStyle:NSNumberFormatterDecimalStyle];
+ [fmt setMinimum:[NSNumber numberWithDouble:[low doubleValue]]];
+ [fmt setMaximum:[NSNumber numberWithDouble:[high doubleValue]]];
+ [fmt setMinimumFractionDigits: (float_p ? 1 : 0)];
+ [fmt setMaximumFractionDigits: (float_p ? 2 : 0)];
+
+ [fmt setGeneratesDecimalNumbers:float_p];
+ [[txt cell] setFormatter:fmt];
+
+ [self bindSwitch:step cmdline:arg];
+ [self bindSwitch:txt cmdline:arg];
+
+ [step release];
+ [txt release];
+
+# endif // USE_IPHONE
+
+ } else {
+ NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label);
+ }
+}
+
+
+# ifndef USE_IPHONE
+static void
+set_menu_item_object (NSMenuItem *item, NSObject *obj)
+{
+ /* If the object associated with this menu item looks like a boolean,
+ store an NSNumber instead of an NSString, since that's what
+ will be in the preferences (due to similar logic in PrefsReader).
+ */
+ if ([obj isKindOfClass:[NSString class]]) {
+ NSString *string = (NSString *) obj;
+ if (NSOrderedSame == [string caseInsensitiveCompare:@"true"] ||
+ NSOrderedSame == [string caseInsensitiveCompare:@"yes"])
+ obj = [NSNumber numberWithBool:YES];
+ else if (NSOrderedSame == [string caseInsensitiveCompare:@"false"] ||
+ NSOrderedSame == [string caseInsensitiveCompare:@"no"])
+ obj = [NSNumber numberWithBool:NO];
+ else
+ obj = string;
+ }
+
+ [item setRepresentedObject:obj];
+ //NSLog (@"menu item \"%@\" = \"%@\" %@", [item title], obj, [obj class]);
+}
+# endif // !USE_IPHONE
+
+
+/* Creates the popup menu described by the given XML node (and its children).
+ */
+- (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent
+{
+ NSArray *children = [node children];
+ NSUInteger i, count = [children count];
+
+ if (count <= 0) {
+ NSAssert1 (0, @"no menu items in \"%@\"", [node name]);
+ return;
+ }
+
+ // get the "id" attribute off the