/* xscreensaver, Copyright (c) 2006-2017 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation. No representations are made about the suitability of this * software for any purpose. It is provided "as is" without express or * implied warranty. */ /* XScreenSaver uses XML files to describe the user interface for configuring the various screen savers. These files live in .../hacks/config/ and say relatively high level things like: "there should be a checkbox labelled "Leave Trails", and when it is checked, add the option '-trails' to the command line when launching the program." This code reads that XML and constructs a Cocoa interface from it. The Cocoa controls are hooked up to NSUserDefaultsController to save those settings into the MacOS preferences system. The Cocoa preferences names are the same as the resource names specified in the screenhack's 'options' array (we use that array to map the command line switches specified in the XML to the resource names to use). */ #import "XScreenSaverConfigSheet.h" #import "Updater.h" #import "jwxyz.h" #import "InvertedSlider.h" #ifdef USE_IPHONE # define NSView UIView # define NSRect CGRect # define NSSize CGSize # define NSTextField UITextField # define NSButton UIButton # define NSFont UIFont # define NSStepper UIStepper # define NSMenuItem UIMenuItem # define NSText UILabel # define minValue minimumValue # define maxValue maximumValue # define setMinValue setMinimumValue # define setMaxValue setMaximumValue # define LABEL UILabel #else # define LABEL NSTextField #endif // USE_IPHONE #undef LABEL_ABOVE_SLIDER #define USE_HTML_LABELS #pragma mark XML Parser /* I used to use the "NSXMLDocument" XML parser, but that doesn't exist on iOS. The "NSXMLParser" parser exists on both OSX and iOS, so I converted to use that. However, to avoid having to re-write all of the old code, I faked out a halfassed implementation of the "NSXMLNode" class that "NSXMLDocument" used to return. */ #define NSXMLNode SimpleXMLNode #define NSXMLElement SimpleXMLNode #define NSXMLCommentKind SimpleXMLCommentKind #define NSXMLElementKind SimpleXMLElementKind #define NSXMLAttributeKind SimpleXMLAttributeKind #define NSXMLTextKind SimpleXMLTextKind typedef enum { SimpleXMLCommentKind, SimpleXMLElementKind, SimpleXMLAttributeKind, SimpleXMLTextKind, } SimpleXMLKind; @interface SimpleXMLNode : NSObject { SimpleXMLKind kind; NSString *name; SimpleXMLNode *parent; NSMutableArray *children; NSMutableArray *attributes; id object; } @property(nonatomic) SimpleXMLKind kind; @property(nonatomic, retain) NSString *name; @property(nonatomic, retain) SimpleXMLNode *parent; @property(nonatomic, retain) NSMutableArray *children; @property(nonatomic, retain) NSMutableArray *attributes; @property(nonatomic, retain, getter=objectValue, setter=setObjectValue:) id object; @end @implementation SimpleXMLNode @synthesize kind; @synthesize name; //@synthesize parent; @synthesize children; @synthesize attributes; @synthesize object; - (id) init { self = [super init]; attributes = [NSMutableArray arrayWithCapacity:10]; return self; } - (id) initWithName:(NSString *)n { self = [self init]; [self setKind:NSXMLElementKind]; [self setName:n]; return self; } - (void) setAttributesAsDictionary:(NSDictionary *)dict { for (NSString *key in dict) { NSObject *val = [dict objectForKey:key]; SimpleXMLNode *n = [[SimpleXMLNode alloc] init]; [n setKind:SimpleXMLAttributeKind]; [n setName:key]; [n setObjectValue:val]; [attributes addObject:n]; [n release]; } } - (SimpleXMLNode *) parent { return parent; } - (void) setParent:(SimpleXMLNode *)p { NSAssert (!parent, @"parent already set"); if (!p) return; parent = p; NSMutableArray *kids = [p children]; if (!kids) { kids = [NSMutableArray arrayWithCapacity:10]; [p setChildren:kids]; } [kids addObject:self]; } @end #pragma mark textMode value transformer // A value transformer for mapping "url" to "3" and vice versa in the // "textMode" preference, since NSMatrix uses NSInteger selectedIndex. #ifndef USE_IPHONE @interface TextModeTransformer: NSValueTransformer {} @end @implementation TextModeTransformer + (Class)transformedValueClass { return [NSString class]; } + (BOOL)allowsReverseTransformation { return YES; } - (id)transformedValue:(id)value { if ([value isKindOfClass:[NSString class]]) { int i = -1; if ([value isEqualToString:@"date"]) { i = 0; } else if ([value isEqualToString:@"literal"]) { i = 1; } else if ([value isEqualToString:@"file"]) { i = 2; } else if ([value isEqualToString:@"url"]) { i = 3; } else if ([value isEqualToString:@"program"]) { i = 4; } if (i != -1) value = [NSNumber numberWithInt: i]; } return value; } - (id)reverseTransformedValue:(id)value { if ([value isKindOfClass:[NSNumber class]]) { switch ((int) [value doubleValue]) { case 0: value = @"date"; break; case 1: value = @"literal"; break; case 2: value = @"file"; break; case 3: value = @"url"; break; case 4: value = @"program"; break; } } return value; } @end #endif // USE_IPHONE #pragma mark Implementing radio buttons /* The UIPickerView is a hideous and uncustomizable piece of shit. I can't believe Apple actually released that thing on the world. Let's fake up some radio buttons instead. */ #if defined(USE_IPHONE) && !defined(USE_PICKER_VIEW) @interface RadioButton : UILabel { int index; NSArray *items; } @property(nonatomic) int index; @property(nonatomic, retain) NSArray *items; @end @implementation RadioButton @synthesize index; @synthesize items; - (id) initWithIndex:(int)_index items:_items { self = [super initWithFrame:CGRectZero]; index = _index; items = [_items retain]; [self setText: [[items objectAtIndex:index] objectAtIndex:0]]; [self setBackgroundColor:[UIColor clearColor]]; [self sizeToFit]; return self; } @end # endif // !USE_PICKER_VIEW # pragma mark Implementing labels with clickable links #if defined(USE_IPHONE) && defined(USE_HTML_LABELS) @interface HTMLLabel : UIView { 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 //

*/ //
NSRect rect; rect.size.width = rect.size.height = 1; rect.origin.x = rect.origin.y = 0; NSView *group = [[NSView alloc] initWithFrame:rect]; NSXMLElement *node2; // node2 = [[NSXMLElement alloc] initWithName:@"boolean"]; [node2 setAttributesAsDictionary: @{ @"id": @SUSUEnableAutomaticChecksKey, @"_label": @"Automatically check for updates", @"arg-unset": @"-no-" SUSUEnableAutomaticChecksKey, }]; [self makeCheckbox:node2 on:group]; [node2 release]; //