X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?p=xscreensaver;a=blobdiff_plain;f=OSX%2FXScreenSaverConfigSheet.m;h=46b73dee9a1a6e56ccfcab3321d43018e0a71476;hp=2b0cb916bf35a9e2269ab3d6a35d7f439de11863;hb=019de959b265701cd0c3fccbb61f2b69f06bf9ee;hpb=ec8d2b32b63649e6d32bdfb306eda062769af823 diff --git a/OSX/XScreenSaverConfigSheet.m b/OSX/XScreenSaverConfigSheet.m index 2b0cb916..46b73dee 100644 --- a/OSX/XScreenSaverConfigSheet.m +++ b/OSX/XScreenSaverConfigSheet.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski +/* xscreensaver, Copyright (c) 2006-2013 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 @@ -24,35 +24,455 @@ */ #import "XScreenSaverConfigSheet.h" +#import "Updater.h" #import "jwxyz.h" #import "InvertedSlider.h" -#import + +#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]; + } +} + +- (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 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 = (UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight); + webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight); + [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 loadHTMLString:h2 baseURL:[NSURL URLWithString:@""]]; +} + + +static char *anchorize (const char *url); + +- (void) setText: (NSString *)t +{ + 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]; + } + [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]; + [self setHTML: html]; + [webView reload]; +} + + +- (NSString *) stripTags:(NSString *)str +{ + NSString *result = @""; + + str = [str stringByReplacingOccurrencesOfString:@"

" + withString:@"

" + options:NSCaseInsensitiveSearch + range:NSMakeRange(0, [str length])]; + str = [str stringByReplacingOccurrencesOfString:@"
" + withString:@"\n" + options:NSCaseInsensitiveSearch + range:NSMakeRange(0, [str length])]; + + 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]; + } + 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 I can't make that work. + Maybe because it loads async? + */ +# 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 sizeWithFont: font + constrainedToSize: s + lineBreakMode:NSLineBreakByWordWrapping]; + + // GAAAH. Add one more line, or the UIWebView is still scrollable! + // The text is sized right, but it lets you scroll it up anyway. + s.height += [font pointSize]; + + 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 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 + -// redefine these since they don't work when not inside an ObjC method -#undef NSAssert -#undef NSAssert1 -#undef NSAssert2 -#undef NSAssert3 -#define NSAssert(CC,S) do { if (!(CC)) { NSLog(S); }} while(0) -#define NSAssert1(CC,S,A) do { if (!(CC)) { NSLog(S,A); }} while(0) -#define NSAssert2(CC,S,A,B) do { if (!(CC)) { NSLog(S,A,B); }} while(0) -#define NSAssert3(CC,S,A,B,C) do { if (!(CC)) { NSLog(S,A,B,C); }} while(0) +/* 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"). */ -static NSString * -switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, - NSString **val_ret) +- (NSString *) switchToResource:(NSString *)cmdline_switch + opts:(const XrmOptionDescRec *)opts_array + valRet:(NSString **)val_ret { char buf[255]; char *tail = 0; @@ -60,7 +480,7 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, if (! [cmdline_switch getCString:buf maxLength:sizeof(buf) encoding:NSUTF8StringEncoding]) { NSAssert1(0, @"unable to convert %@", cmdline_switch); - abort(); + return 0; } char *s = strpbrk(buf, " \t\r\n"); if (s && *s) { @@ -70,15 +490,15 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, tail++; } - while (opts[0].option) { - if (!strcmp (opts[0].option, buf)) { + while (opts_array[0].option) { + if (!strcmp (opts_array[0].option, buf)) { const char *ret = 0; - if (opts[0].argKind == XrmoptionNoArg) { + if (opts_array[0].argKind == XrmoptionNoArg) { if (tail && *tail) NSAssert1 (0, @"expected no args to switch: \"%@\"", cmdline_switch); - ret = opts[0].value; + ret = opts_array[0].value; } else { if (!tail || !*tail) NSAssert1 (0, @"expected args to switch: \"%@\"", @@ -92,28 +512,180 @@ switch_to_resource (NSString *cmdline_switch, const XrmOptionDescRec *opts, encoding:NSUTF8StringEncoding] : 0); - const char *res = opts[0].specifier; + const char *res = opts_array[0].specifier; while (*res && (*res == '.' || *res == '*')) res++; - return [NSString stringWithCString:res - encoding:NSUTF8StringEncoding]; + return [self makeCKey:res]; } - opts++; + opts_array++; } NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch); - abort(); + 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 +{ + [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. */ -static void -bind_resource_to_preferences (NSUserDefaultsController *prefs, - NSObject *control, - NSString *pref_key, - const XrmOptionDescRec *opts) +- (void) bindResource:(NSObject *)control key:(NSString *)pref_key + reload:(BOOL)reload_p { + NSUserDefaultsController *prefs = [self controllerForKey:pref_key]; +# ifndef USE_IPHONE NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]] ? @"selectedObject" : ([control isKindOfClass:[NSMatrix class]] @@ -123,8 +695,73 @@ bind_resource_to_preferences (NSUserDefaultsController *prefs, toObject:prefs withKeyPath:[@"values." stringByAppendingString: pref_key] options:nil]; +# 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 0 // #### + 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]; @@ -134,451 +771,456 @@ bind_resource_to_preferences (NSUserDefaultsController *prefs, # endif } -static void -bind_switch_to_preferences (NSUserDefaultsController *prefs, - NSObject *control, - NSString *cmdline_switch, - const XrmOptionDescRec *opts) + +- (void) bindResource:(NSObject *)control key:(NSString *)pref_key { - NSString *pref_key = switch_to_resource (cmdline_switch, opts, 0); - bind_resource_to_preferences (prefs, control, pref_key, opts); + [self bindResource:(NSObject *)control key:(NSString *)pref_key reload:NO]; } -/* 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. - */ -static void -parse_attrs (NSMutableDictionary *dict, NSXMLNode *node) + +- (void) bindSwitch:(NSObject *)control + cmdline:(NSString *)cmd { - NSArray *attrs = [(NSXMLElement *) node attributes]; - int 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]; - } + [self bindResource:control + key:[self switchToResource:cmd opts:opts valRet:0]]; } -/* Creates a label: an un-editable NSTextField displaying the given text. - */ -static NSTextField * -make_label (NSString *text) -{ - NSRect rect; - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; - NSTextField *lab = [[NSTextField alloc] initWithFrame:rect]; - [lab setSelectable:NO]; - [lab setEditable:NO]; - [lab setBezeled:NO]; - [lab setDrawsBackground:NO]; - [lab setStringValue:text]; - [lab sizeToFit]; - return lab; -} +#pragma mark Text-manipulating utilities -static NSView * -last_child (NSView *parent) +static NSString * +unwrap (NSString *text) { - NSArray *kids = [parent subviews]; - int nkids = [kids count]; - if (nkids == 0) - return 0; - else - return [kids objectAtIndex:nkids-1]; -} + // Unwrap lines: delete \n but do not delete \n\n. + // + NSArray *lines = [text componentsSeparatedByString:@"\n"]; + int nlines = [lines count]; + BOOL eolp = YES; + int i; + text = @"\n"; // start with one blank line -/* Add the child as a subview of the parent, positioning it immediately - below or to the right of the previously-added child of that view. - */ -static void -place_child (NSView *parent, NSView *child, BOOL right_p) -{ - NSRect rect = [child frame]; - NSView *last = last_child (parent); - if (!last) { - rect.origin.x = LEFT_MARGIN; - rect.origin.y = [parent frame].size.height - rect.size.height - - LINE_SPACING; - } else if (right_p) { - rect = [last frame]; - rect.origin.x += rect.size.width + COLUMN_SPACING; - } else { - rect = [last frame]; - rect.origin.x = LEFT_MARGIN; - rect.origin.y -= [child frame].size.height + LINE_SPACING; + // 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--; } - [child setFrameOrigin:rect.origin]; - [parent addSubview:child]; -} + // skip leading blank lines in file + for (i = 0; i < nlines; i++) { + NSString *s = (NSString *) [lines objectAtIndex:i]; + if ([s length] > 0) + break; + } -static void traverse_children (NSUserDefaultsController *, - const XrmOptionDescRec *, - NSView *, NSXMLNode *); + // 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:@"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; +} -/* Creates the checkbox (NSButton) described by the given XML node. +# ifndef USE_IPHONE +/* Makes the text up to the first comma be bold. */ static void -make_checkbox (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"id", - @"", @"_label", - @"", @"arg-set", - @"", @"arg-unset", - nil]; - parse_attrs (dict, node); - NSString *label = [dict objectForKey:@"_label"]; - NSString *arg_set = [dict objectForKey:@"arg-set"]; - NSString *arg_unset = [dict objectForKey:@"arg-unset"]; - - 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; +boldify (NSText *nstext) +{ + NSString *text = [nstext string]; + NSRange r = [text rangeOfString:@"," options:0]; + r.length = r.location+1; - NSButton *button = [[NSButton alloc] initWithFrame:rect]; - [button setButtonType:([[node name] isEqualToString:@"radio"] - ? NSRadioButton - : NSSwitchButton)]; - [button setTitle:label]; - [button sizeToFit]; - place_child (parent, button, NO); - - bind_switch_to_preferences (prefs, button, - (arg_set ? arg_set : arg_unset), - opts); - [button release]; + r.location = 0; + + NSFont *font = [nstext font]; + font = [NSFont boldSystemFontOfSize:[font pointSize]]; + [nstext setFont:font range:r]; } +# endif // !USE_IPHONE -/* Creates the NSTextField described by the given XML node. -*/ -static void -make_text_field (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, - BOOL no_label_p) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"id", - @"", @"_label", - @"", @"arg", - nil]; - parse_attrs (dict, node); - NSString *label = [dict objectForKey:@"_label"]; - NSString *arg = [dict objectForKey:@"arg"]; +/* Creates a human-readable anchor to put on a URL. + */ +static char * +anchorize (const char *url) +{ + const char *wiki = "http://en.wikipedia.org/wiki/"; + const char *math = "http://mathworld.wolfram.com/"; + if (!strncmp (wiki, url, strlen(wiki))) { + char *anchor = (char *) malloc (strlen(url) * 3 + 10); + strcpy (anchor, "Wikipedia: \""); + const char *in = url + strlen(wiki); + 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; - if (!label && !no_label_p) { - NSAssert1 (0, @"no _label in %@", [node name]); - return; + } else if (!strncmp (math, url, strlen(math))) { + char *anchor = (char *) malloc (strlen(url) * 3 + 10); + strcpy (anchor, "MathWorld: \""); + const char *start = url + strlen(wiki); + 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); } +} - NSAssert1 (arg, @"no arg in %@", label); - NSRect rect; - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; - - NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; +#if !defined(USE_IPHONE) || !defined(USE_HTML_LABELS) - // make the default size be around 30 columns; a typical value for - // these text fields is "xscreensaver-text --cols 40". - // - [txt setStringValue:@"123456789 123456789 123456789 "]; - [txt sizeToFit]; - [[txt cell] setWraps:NO]; - [[txt cell] setScrollable:YES]; - [txt setStringValue:@""]; - - if (label) { - NSTextField *lab = make_label (label); - place_child (parent, lab, NO); - [lab release]; - } +/* 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 - place_child (parent, txt, (label ? YES : NO)); + int L = [text length]; + NSRange start; // range is start-of-search to end-of-string + start.location = 0; + start.length = L; + while (start.location < L) { - bind_switch_to_preferences (prefs, txt, arg, opts); - [txt release]; -} + // Find the beginning of a URL... + // + NSRange r2 = [text rangeOfString:@"http://" options:0 range:start]; + if (r2.location == NSNotFound) + break; + // Next time around, start searching after this. + start.location = r2.location + r2.length; + start.length = L - start.location; -/* Creates the NSTextField described by the given XML node, - and hooks it up to a Choose button and a file selector widget. -*/ -static void -make_file_selector (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node, - BOOL dirs_only_p, - BOOL no_label_p, - BOOL editable_text_p) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"id", - @"", @"_label", - @"", @"arg", - nil]; - parse_attrs (dict, node); - NSString *label = [dict objectForKey:@"_label"]; - NSString *arg = [dict objectForKey:@"arg"]; + // Find the end of a URL (whitespace or EOF)... + // + NSRange r3 = [text rangeOfCharacterFromSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet] + options:0 range:start]; + if (r3.location == NSNotFound) // EOF + r3.location = L, r3.length = 0; - if (!label && !no_label_p) { - NSAssert1 (0, @"no _label in %@", [node name]); - return; - } + // Next time around, start searching after this. + start.location = r3.location; + start.length = L - start.location; - NSAssert1 (arg, @"no arg in %@", label); + // Set r2 to the start/length of this URL. + r2.length = start.location - r2.location; - NSRect rect; - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; - - NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; + // Extract the URL. + NSString *nsurl = [text substringWithRange:r2]; + const char *url = [nsurl UTF8String]; - // make the default size be around 20 columns. - // - [txt setStringValue:@"123456789 123456789 "]; - [txt sizeToFit]; - [txt setSelectable:YES]; - [txt setEditable:editable_text_p]; - [txt setBezeled:editable_text_p]; - [txt setDrawsBackground:editable_text_p]; - [[txt cell] setWraps:NO]; - [[txt cell] setScrollable:YES]; - [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead]; - [txt setStringValue:@""]; + // If this is a Wikipedia URL, make the linked text be prettier. + // + char *anchor = anchorize(url); - NSTextField *lab = 0; - if (label) { - lab = make_label (label); - place_child (parent, lab, NO); - [lab release]; - } +# ifndef USE_IPHONE - place_child (parent, txt, (label ? YES : NO)); + // 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); - bind_switch_to_preferences (prefs, txt, arg, opts); - [txt release]; + NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)]; + [nstext replaceCharactersInRange:r2 withRTF:rtfdata]; - // Make the text field and label be the same height, whichever is taller. - if (lab) { - rect = [txt frame]; - rect.size.height = ([lab frame].size.height > [txt frame].size.height - ? [lab frame].size.height - : [txt frame].size.height); - [txt setFrame:rect]; +# 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); + + int L2 = [text length]; // might have changed + start.location -= (L - L2); + L = L2; } - // Now put a "Choose" button next to it. - // - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; - NSButton *choose = [[NSButton alloc] initWithFrame:rect]; - [choose setTitle:@"Choose..."]; - [choose setBezelStyle:NSRoundedBezelStyle]; - [choose sizeToFit]; +# ifdef USE_IPHONE + [nstext setText:text]; + [nstext sizeToFit]; +# endif +} - place_child (parent, choose, YES); +#endif /* !USE_IPHONE || !USE_HTML_LABELS */ - // center the Choose button around the midpoint of the text field. - rect = [choose frame]; - rect.origin.y = ([txt frame].origin.y + - (([txt frame].size.height - rect.size.height) / 2)); - [choose setFrameOrigin:rect.origin]; - [choose setTarget:[parent window]]; - if (dirs_only_p) - [choose setAction:@selector(chooseClickedDirs:)]; - else - [choose setAction:@selector(chooseClicked:)]; - [choose release]; -} +#pragma mark Creating controls from XML -/* Runs a modal file selector and sets the text field's value to the - selected file or directory. +/* 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. */ -static void -do_file_selector (NSTextField *txt, BOOL dirs_p) +- (void) parseAttrs:(NSMutableDictionary *)dict node:(NSXMLNode *)node { - NSOpenPanel *panel = [NSOpenPanel openPanel]; - [panel setAllowsMultipleSelection:NO]; - [panel setCanChooseFiles:!dirs_p]; - [panel setCanChooseDirectories:dirs_p]; - - NSString *file = [txt stringValue]; - if ([file length] <= 0) { - file = NSHomeDirectory(); - if (dirs_p) - file = [file stringByAppendingPathComponent:@"Pictures"]; + NSArray *attrs = [(NSXMLElement *) node attributes]; + int 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]; } -// NSString *dir = [file stringByDeletingLastPathComponent]; - - int result = [panel runModalForDirectory:file //dir - file:nil //[file lastPathComponent] - types:nil]; - if (result == NSOKButton) { - NSArray *files = [panel filenames]; - file = ([files count] > 0 ? [files objectAtIndex:0] : @""); - file = [file stringByAbbreviatingWithTildeInPath]; - [txt setStringValue:file]; - - // Fuck me! Just setting the value of the NSTextField does not cause - // that to end up in the preferences! - // - NSDictionary *dict = [txt infoForBinding:@"value"]; - NSUserDefaultsController *prefs = [dict objectForKey:@"NSObservedObject"]; - NSString *path = [dict objectForKey:@"NSObservedKeyPath"]; - if ([path hasPrefix:@"values."]) // WTF. - path = [path substringFromIndex:7]; - [[prefs values] setValue:file forKey:path]; - -#if 0 - // make sure the end of the string is visible. - NSText *fe = [[txt window] fieldEditor:YES forObject:txt]; - NSRange range; - range.location = [file length]-3; - range.length = 1; - if (! [[txt window] makeFirstResponder:[txt window]]) - [[txt window] endEditingFor:nil]; -// [[txt window] makeFirstResponder:nil]; - [fe setSelectedRange:range]; -// [tv scrollRangeToVisible:range]; -// [txt setNeedsDisplay:YES]; -// [[txt cell] setNeedsDisplay:YES]; -// [txt selectAll:txt]; -#endif +# 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 } -/* Returns the NSTextField that is to the left of or above the NSButton. + +/* Handle the options on the top level tag. */ -static NSTextField * -find_text_field_of_button (NSButton *button) +- (NSString *) parseXScreenSaverTag:(NSXMLNode *)node { - NSView *parent = [button superview]; - NSArray *kids = [parent subviews]; - int nkids = [kids count]; - int i; - NSTextField *f = 0; - for (i = 0; i < nkids; i++) { - NSObject *kid = [kids objectAtIndex:i]; - if ([kid isKindOfClass:[NSTextField class]]) { - f = (NSTextField *) kid; - } else if (kid == button) { - if (! f) abort(); - return f; - } - } - abort(); + NSMutableDictionary *dict = [@{ @"name": @"", + @"_label": @"", + @"gl": @"" } + mutableCopy]; + [self parseAttrs:dict node:node]; + NSString *name = [dict objectForKey:@"name"]; + NSString *label = [dict objectForKey:@"_label"]; + + NSAssert1 (label, @"no _label in %@", [node name]); + NSAssert1 (name, @"no name in \"%@\"", label); + return label; } -- (void) chooseClicked:(NSObject *)arg +/* Creates a label: an un-editable NSTextField displaying the given text. + */ +- (LABEL *) makeLabel:(NSString *)text { - NSButton *choose = (NSButton *) arg; - NSTextField *txt = find_text_field_of_button (choose); - do_file_selector (txt, NO); + 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 + return lab; } -- (void) chooseClickedDirs:(NSObject *)arg + +/* Creates the checkbox (NSButton) described by the given XML node. + */ +- (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent { - NSButton *choose = (NSButton *) arg; - NSTextField *txt = find_text_field_of_button (choose); - do_file_selector (txt, YES); + 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"]; + + 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]; + [lab release]; + +# 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. -*/ -static void -make_number_selector (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"id", - @"", @"_label", - @"", @"_low-label", - @"", @"_high-label", - @"", @"type", - @"", @"arg", - @"", @"low", - @"", @"high", - @"", @"default", - @"", @"convert", - nil]; - parse_attrs (dict, node); + */ +- (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"]; @@ -618,18 +1260,20 @@ make_number_selector (NSUserDefaultsController *prefs, BOOL float_p = ([low rangeOfCharacterFromSet:dot].location != NSNotFound || [high rangeOfCharacterFromSet:dot].location != NSNotFound); - if ([type isEqualToString:@"slider"]) { + 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; - if (cvt) - slider = [[InvertedSlider alloc] initWithFrame:rect]; - else - slider = [[NSSlider alloc] initWithFrame:rect]; - + slider = [[InvertedSlider alloc] initWithFrame:rect + inverted: !!cvt + integers: !float_p]; [slider setMaxValue:[high doubleValue]]; [slider setMinValue:[low doubleValue]]; @@ -647,38 +1291,64 @@ make_number_selector (NSUserDefaultsController *prefs, if (float_p && range2 < max_ticks) range2 = max_ticks; +# ifndef USE_IPHONE [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) { - NSTextField *lab = make_label (label); - place_child (parent, lab, NO); + 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 [lab release]; } if (low_label) { - NSTextField *lab = make_label (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]; - place_child (parent, lab, NO); + [self placeChild:lab on:parent]; +# else // USE_IPHONE + [lab setTextAlignment: NSTextAlignmentRight]; + [self placeChild:lab on:parent right:(label ? YES : NO)]; +# endif // USE_IPHONE + [lab release]; } - place_child (parent, slider, (low_label ? YES : NO)); +# 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) @@ -687,18 +1357,22 @@ make_number_selector (NSUserDefaultsController *prefs, } if (high_label) { - NSTextField *lab = make_label (high_label); - [lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + 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]; - place_child (parent, lab, YES); + [self placeChild:lab on:parent right:YES]; [lab release]; } - bind_switch_to_preferences (prefs, slider, arg, opts); + [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) { @@ -722,19 +1396,19 @@ make_number_selector (NSUserDefaultsController *prefs, [txt setStringValue:@""]; if (label) { - NSTextField *lab = make_label (label); - //[lab setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + 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]; - place_child (parent, lab, NO); + [self placeChild:lab on:parent]; [lab release]; } - place_child (parent, txt, (label ? YES : NO)); + [self placeChild:txt on:parent right:(label ? YES : NO)]; if (! label) { rect = [txt frame]; @@ -746,7 +1420,7 @@ make_number_selector (NSUserDefaultsController *prefs, rect.size.width = rect.size.height = 10; NSStepper *step = [[NSStepper alloc] initWithFrame:rect]; [step sizeToFit]; - place_child (parent, step, YES); + [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; @@ -776,19 +1450,21 @@ make_number_selector (NSUserDefaultsController *prefs, [fmt setGeneratesDecimalNumbers:float_p]; [[txt cell] setFormatter:fmt]; - - bind_switch_to_preferences (prefs, step, arg, opts); - bind_switch_to_preferences (prefs, txt, arg, opts); + [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) { @@ -811,14 +1487,12 @@ set_menu_item_object (NSMenuItem *item, NSObject *obj) [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). -*/ -static void -make_option_menu (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) + */ +- (void) makeOptionMenu:(NSXMLNode *)node on:(NSView *)parent { NSArray *children = [node children]; int i, count = [children count]; @@ -830,28 +1504,40 @@ make_option_menu (NSUserDefaultsController *prefs, // 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]; + + //