X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=OSX%2FXScreenSaverConfigSheet.m;h=ebf33827ea9747ba397588fbe336cfe45a1bc529;hb=4ade52359b6eba3621566dac79793a33aa4c915f;hp=24ab1c42558c3acfb950d8afbf8b84779192bf81;hpb=49f5b54f312fe4ac2e9bc47581a72451bd0e8439;p=xscreensaver diff --git a/OSX/XScreenSaverConfigSheet.m b/OSX/XScreenSaverConfigSheet.m index 24ab1c42..ebf33827 100644 --- a/OSX/XScreenSaverConfigSheet.m +++ b/OSX/XScreenSaverConfigSheet.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006 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 @@ -27,40 +27,459 @@ #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; NSAssert(cmdline_switch, @"cmdline switch is null"); if (! [cmdline_switch getCString:buf maxLength:sizeof(buf) encoding:NSUTF8StringEncoding]) { - NSAssert1(0, @"unable to convert %@\n", cmdline_switch); - abort(); + NSAssert1(0, @"unable to convert %@", cmdline_switch); + return 0; } char *s = strpbrk(buf, " \t\r\n"); if (s && *s) { @@ -70,18 +489,18 @@ 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: \"%@\"\n", + 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: \"%@\"\n", + NSAssert1 (0, @"expected args to switch: \"%@\"", cmdline_switch); ret = tail; } @@ -92,464 +511,676 @@ 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\n", cmdline_switch); - abort(); + NSAssert1 (0, @"\"%@\" not present in options", cmdline_switch); + return 0; +} + + +#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]); + + if (v == (int) v) + [userDefaultsController setInteger:v forKey:pref_key]; + else + [userDefaultsController setDouble: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"); + [userDefaultsController 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]; + [userDefaultsController 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]; + [userDefaultsController 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]; + [userDefaultsController 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]; + [userDefaultsController save:self]; + [NSApp endSheet:self returnCode:NSOKButton]; + [self close]; +} + +- (void) cancelAction:(NSObject *)arg +{ + [userDefaultsController revert:self]; + [NSApp endSheet:self returnCode:NSCancelButton]; + [self close]; +} +# endif // !USE_IPHONE + + +- (void) resetAction:(NSObject *)arg +{ +# ifndef USE_IPHONE + [userDefaultsController revertToInitialValues:self]; +# else // USE_IPHONE + + for (NSString *key in defaultOptions) { + NSObject *val = [defaultOptions objectForKey:key]; + [userDefaultsController 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 { +# ifndef USE_IPHONE NSString *bindto = ([control isKindOfClass:[NSPopUpButton class]] ? @"selectedObject" : ([control isKindOfClass:[NSMatrix class]] ? @"selectedIndex" : @"value")); [control bind:bindto - toObject:prefs + toObject:userDefaultsController withKeyPath:[@"values." stringByAppendingString: pref_key] options:nil]; +# else // USE_IPHONE + SEL sel; + NSObject *val = [userDefaultsController 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]; + } + } -# if 0 // #### - NSObject *def = [[prefs defaults] objectForKey:pref_key]; +# endif // USE_IPHONE + +# if 0 + NSObject *def = [[userDefaultsController defaults] objectForKey:pref_key]; NSString *s = [NSString stringWithFormat:@"bind: \"%@\"", pref_key]; s = [s stringByPaddingToLength:18 withString:@" " startingAtIndex:0]; s = [NSString stringWithFormat:@"%@ = \"%@\"", s, def]; s = [s stringByPaddingToLength:28 withString:@" " startingAtIndex:0]; - NSLog (@"%@ %@/%@\n", s, [def class], [control class]); + NSLog (@"%@ %@/%@", s, [def class], [control class]); # 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 \"%@\"\n", key, [node name]); - } else if ([old length] != 0) { - NSAssert2 (0, @"duplicate %@: \"%@\", \"%@\"\n", 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) +boldify (NSText *nstext) { - 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 %@\n", [node name]); - return; - } - if (!arg_set && !arg_unset) { - NSAssert1 (0, @"neither arg-set nor arg-unset provided in \"%@\"\n", - label); - } - if (arg_set && arg_unset) { - NSAssert1 (0, @"only one of arg-set and arg-unset may be used in \"%@\"\n", - 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 \"%@\": %@\n", - label, arg_set); - if (arg_unset && (![arg_unset hasPrefix:@"-no-"] && - ![arg_unset hasPrefix:@"--no-"])) - NSLog(@"arg-unset should be a \"no\" option in \"%@\": %@\n", - label, arg_unset); - - NSRect rect; - rect.origin.x = rect.origin.y = 0; - rect.size.width = rect.size.height = 10; + 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) +/* Creates a human-readable anchor to put on a URL. + */ +static char * +anchorize (const char *url) { - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"id", - @"", @"_label", - @"", @"arg", - nil]; - parse_attrs (dict, node); - NSString *label = [dict objectForKey:@"_label"]; - NSString *arg = [dict objectForKey:@"arg"]; + 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; + + } 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; - if (!label && !no_label_p) { - NSAssert1 (0, @"no _label in %@\n", [node name]); - return; + } else { + return strdup (url); } +} - NSAssert1 (arg, @"no arg in %@\n", 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) -{ - 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 %@\n", [node name]); - return; - } + // Next time around, start searching after this. + start.location = r3.location; + start.length = L - start.location; - NSAssert1 (arg, @"no arg in %@\n", 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:NO]; - [txt setBezeled:NO]; - [txt setDrawsBackground:NO]; - [[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 + + // 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); - place_child (parent, txt, (label ? YES : NO)); + NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)]; + [nstext replaceCharactersInRange:r2 withRTF:rtfdata]; - bind_switch_to_preferences (prefs, txt, arg, opts); - [txt release]; +# 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 - // Make the text field be the same height as the label. - if (lab) { - rect = [txt frame]; - rect.size.height = [lab frame].size.height; - [txt setFrame:rect]; + 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]; + 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 *file = [txt stringValue]; - if ([file length] <= 0) { - file = NSHomeDirectory(); - if (dirs_p) - file = [file stringByAppendingPathComponent:@"Pictures"]; +# 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 +} -// NSString *dir = [file stringByDeletingLastPathComponent]; - int result = [panel runModalForDirectory:file //dir - file:nil //[file lastPathComponent] - types:nil]; - if (result == NSOKButton) { - NSArray *files = [panel filenames]; - NSString *file = ([files count] > 0 ? [files objectAtIndex:0] : @""); - file = [file stringByAbbreviatingWithTildeInPath]; - [txt setStringValue:file]; +/* Handle the options on the top level tag. + */ +- (NSString *) parseXScreenSaverTag:(NSXMLNode *)node +{ + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"", @"name", + @"", @"_label", + @"", @"gl", + nil]; + [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; +} - // 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 - } +/* 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 + return lab; } -/* Returns the NSTextField that is to the left of or above the NSButton. + +/* Creates the checkbox (NSButton) described by the given XML node. */ -static NSTextField * -find_text_field_of_button (NSButton *button) +- (void) makeCheckbox:(NSXMLNode *)node on:(NSView *)parent { - 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; - } + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"", @"id", + @"", @"_label", + @"", @"arg-set", + @"", @"arg-unset", + nil]; + [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; } - abort(); -} + 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 -- (void) chooseClicked:(NSObject *)arg -{ - NSButton *choose = (NSButton *) arg; - NSTextField *txt = find_text_field_of_button (choose); - do_file_selector (txt, NO); -} + NSButton *button = [[NSButton alloc] initWithFrame:rect]; + [button setButtonType:NSSwitchButton]; + [button setTitle:label]; + [button sizeToFit]; + [self placeChild:button on:parent]; -- (void) chooseClickedDirs:(NSObject *)arg -{ - NSButton *choose = (NSButton *) arg; - NSTextField *txt = find_text_field_of_button (choose); - do_file_selector (txt, YES); +# 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]; } @@ -557,10 +1188,7 @@ find_text_field_of_button (NSButton *button) 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) +- (void) makeNumberSelector:(NSXMLNode *)node on:(NSView *)parent { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys: @@ -575,7 +1203,7 @@ make_number_selector (NSUserDefaultsController *prefs, @"", @"default", @"", @"convert", nil]; - parse_attrs (dict, node); + [self parseAttrs:dict node:node]; NSString *label = [dict objectForKey:@"_label"]; NSString *low_label = [dict objectForKey:@"_low-label"]; NSString *high_label = [dict objectForKey:@"_high-label"]; @@ -586,19 +1214,19 @@ make_number_selector (NSUserDefaultsController *prefs, NSString *def = [dict objectForKey:@"default"]; NSString *cvt = [dict objectForKey:@"convert"]; - NSAssert1 (arg, @"no arg in %@\n", label); - NSAssert1 (type, @"no type in %@\n", label); + NSAssert1 (arg, @"no arg in %@", label); + NSAssert1 (type, @"no type in %@", label); if (! low) { - NSAssert1 (0, @"no low in %@\n", [node name]); + NSAssert1 (0, @"no low in %@", [node name]); return; } if (! high) { - NSAssert1 (0, @"no high in %@\n", [node name]); + NSAssert1 (0, @"no high in %@", [node name]); return; } if (! def) { - NSAssert1 (0, @"no default in %@\n", [node name]); + NSAssert1 (0, @"no default in %@", [node name]); return; } if (cvt && ![cvt isEqualToString:@"invert"]) { @@ -606,42 +1234,104 @@ make_number_selector (NSUserDefaultsController *prefs, label); } - if ([type isEqualToString:@"slider"]) { + // 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 = 20; + 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]]; + int range = [slider maxValue] - [slider minValue] + 1; + int range2 = range; + int max_ticks = 21; + while (range2 > max_ticks) + range2 /= 10; + + // 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; + +# 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) @@ -650,29 +1340,33 @@ 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) { - NSAssert1 (0, @"no _label in spinbutton %@\n", [node name]); + NSAssert1 (0, @"no _label in spinbutton %@", [node name]); return; } NSAssert1 (!low_label, - @"low-label not allowed in spinbutton \"%@\"\n", [node name]); + @"low-label not allowed in spinbutton \"%@\"", [node name]); NSAssert1 (!high_label, - @"high-label not allowed in spinbutton \"%@\"\n", [node name]); - NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"\n", + @"high-label not allowed in spinbutton \"%@\"", [node name]); + NSAssert1 (!cvt, @"convert not allowed in spinbutton \"%@\"", [node name]); NSRect rect; @@ -685,19 +1379,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]; @@ -709,10 +1403,10 @@ 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.size.height = [txt frame].size.height; 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]]; @@ -728,18 +1422,32 @@ make_number_selector (NSUserDefaultsController *prefs, else [step setIncrement:1.0]; - bind_switch_to_preferences (prefs, step, arg, opts); - bind_switch_to_preferences (prefs, txt, arg, opts); + 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 \"%@\"\n", type, label); + NSAssert2 (0, @"unknown type \"%@\" in \"%@\"", type, label); } } +# ifndef USE_IPHONE static void set_menu_item_object (NSMenuItem *item, NSObject *obj) { @@ -762,20 +1470,18 @@ 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]; if (count <= 0) { - NSAssert1 (0, @"no menu items in \"%@\"\n", [node name]); + NSAssert1 (0, @"no menu items in \"%@\"", [node name]); return; } @@ -785,19 +1491,39 @@ make_option_menu (NSUserDefaultsController *prefs, [NSMutableDictionary dictionaryWithObjectsAndKeys: @"", @"id", nil]; - parse_attrs (dict, node); + [self parseAttrs:dict node:node]; NSRect rect; rect.origin.x = rect.origin.y = 0; rect.size.width = 10; rect.size.height = 10; + + NSString *menu_key = nil; // the resource key used by items in this menu + +# ifndef USE_IPHONE + // #### "Build and Analyze" says that all of our widgets leak, because it + // seems to not realize that placeChild -> addSubview retains them. + // Not sure what to do to make these warnings go away. + NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect pullsDown:NO]; - NSMenuItem *def_item = nil; float max_width = 0; - - NSString *menu_key = nil; // the resource key used by items in this menu + +# else // USE_IPHONE + + NSString *def_item = nil; + + rect.size.width = 0; + rect.size.height = 0; +# ifdef USE_PICKER_VIEW + UIPickerView *popup = [[[UIPickerView alloc] initWithFrame:rect] retain]; + popup.delegate = self; + popup.dataSource = self; +# endif // !USE_PICKER_VIEW + NSMutableArray *items = [NSMutableArray arrayWithCapacity:10]; + +# endif // USE_IPHONE for (i = 0; i < count; i++) { NSXMLNode *child = [children objectAtIndex:i]; @@ -805,7 +1531,7 @@ make_option_menu (NSUserDefaultsController *prefs, if ([child kind] == NSXMLCommentKind) continue; if ([child kind] != NSXMLElementKind) { - NSAssert2 (0, @"weird XML node kind: %d: %@", [child kind], node); +// NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[child kind], node); continue; } @@ -817,26 +1543,30 @@ make_option_menu (NSUserDefaultsController *prefs, @"", @"_label", @"", @"arg-set", nil]; - parse_attrs (dict2, child); + [self parseAttrs:dict2 node:child]; NSString *label = [dict2 objectForKey:@"_label"]; NSString *arg_set = [dict2 objectForKey:@"arg-set"]; if (!label) { - NSAssert1 (0, @"no _label in %@\n", [child name]); - return; + NSAssert1 (0, @"no _label in %@", [child name]); + continue; } +# ifndef USE_IPHONE // create the menu item (and then get a pointer to it) [popup addItemWithTitle:label]; NSMenuItem *item = [popup itemWithTitle:label]; +# endif // USE_IPHONE if (arg_set) { NSString *this_val = NULL; - NSString *this_key = switch_to_resource (arg_set, opts, &this_val); + NSString *this_key = [self switchToResource: arg_set + opts: opts + valRet: &this_val]; NSAssert1 (this_val, @"this_val null for %@", arg_set); if (menu_key && ![menu_key isEqualToString:this_key]) NSAssert3 (0, - @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"\n", + @"multiple resources in menu: \"%@\" vs \"%@\" = \"%@\"", menu_key, this_key, this_val); if (this_key) menu_key = this_key; @@ -844,26 +1574,41 @@ make_option_menu (NSUserDefaultsController *prefs, /* If this menu has the cmd line "-mode foo" then set this item's value to "foo" (the menu itself will be bound to e.g. "modeString") */ +# ifndef USE_IPHONE set_menu_item_object (item, this_val); +# else + // Array holds ["Label", "resource-key", "resource-val"]. + [items addObject:[NSMutableArray arrayWithObjects: + label, @"", this_val, nil]]; +# endif } else { // no arg-set -- only one menu item can be missing that. - NSAssert1 (!def_item, @"no arg-set in \"%@\"\n", label); + NSAssert1 (!def_item, @"no arg-set in \"%@\"", label); +# ifndef USE_IPHONE def_item = item; +# else + def_item = label; + // Array holds ["Label", "resource-key", "resource-val"]. + [items addObject:[NSMutableArray arrayWithObjects: + label, @"", @"", nil]]; +# endif } /* make sure the menu button has room for the text of this item, and remember the greatest width it has reached. */ +# ifndef USE_IPHONE [popup setTitle:label]; [popup sizeToFit]; NSRect r = [popup frame]; if (r.size.width > max_width) max_width = r.size.width; +# endif // USE_IPHONE } if (!menu_key) { NSAssert1 (0, @"no switches in menu \"%@\"", [dict objectForKey:@"id"]); - abort(); + return; } /* We've added all of the menu items. If there was an item with no @@ -873,16 +1618,33 @@ make_option_menu (NSUserDefaultsController *prefs, yet know what resource was associated with this menu.) */ if (def_item) { - NSDictionary *defs = [prefs initialValues]; - NSObject *def_obj = [defs objectForKey:menu_key]; - + NSObject *def_obj = [defaultOptions objectForKey:menu_key]; NSAssert2 (def_obj, @"no default value for resource \"%@\" in menu item \"%@\"", - menu_key, [def_item title]); + menu_key, +# ifndef USE_IPHONE + [def_item title] +# else + def_item +# endif + ); +# ifndef USE_IPHONE set_menu_item_object (def_item, def_obj); +# else // !USE_IPHONE + for (NSMutableArray *a in items) { + // Make sure each array contains the resource key. + [a replaceObjectAtIndex:1 withObject:menu_key]; + // Make sure the default item contains the default resource value. + if (def_obj && def_item && + [def_item isEqualToString:[a objectAtIndex:0]]) + [a replaceObjectAtIndex:2 withObject:def_obj]; + } +# endif // !USE_IPHONE } +# ifndef USE_IPHONE +# ifdef USE_PICKER_VIEW /* Finish tweaking the menu button itself. */ if (def_item) @@ -890,22 +1652,54 @@ make_option_menu (NSUserDefaultsController *prefs, NSRect r = [popup frame]; r.size.width = max_width; [popup setFrame:r]; - place_child (parent, popup, NO); +# endif // USE_PICKER_VIEW +# endif - bind_resource_to_preferences (prefs, popup, menu_key, opts); +# if !defined(USE_IPHONE) || defined(USE_PICKER_VIEW) + [self placeChild:popup on:parent]; + [self bindResource:popup key:menu_key]; [popup release]; -} +# endif + +# ifdef USE_IPHONE +# ifdef USE_PICKER_VIEW + // Store the items for this picker in the picker_values array. + // This is so fucking stupid. + + int menu_number = [pref_keys count] - 1; + if (! picker_values) + picker_values = [[NSMutableArray arrayWithCapacity:menu_number] retain]; + while ([picker_values count] <= menu_number) + [picker_values addObject:[NSArray arrayWithObjects: nil]]; + [picker_values replaceObjectAtIndex:menu_number withObject:items]; + [popup reloadAllComponents]; + +# else // !USE_PICKER_VIEW + + [self placeSeparator]; + + i = 0; + for (NSArray *item in items) { + RadioButton *b = [[RadioButton alloc] initWithIndex:i + items:items]; + [b setLineBreakMode:NSLineBreakByTruncatingHead]; + [b setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]]; + [self placeChild:b on:parent]; + i++; + } + + [self placeSeparator]; +# endif // !USE_PICKER_VIEW +# endif // !USE_IPHONE + +} -static NSString *unwrap (NSString *); -static void hreffify (NSText *); -static void boldify (NSText *); /* Creates an uneditable, wrapping NSTextField to display the given text enclosed by ... in the XML. */ -static void -make_desc_label (NSView *parent, NSXMLNode *node) +- (void) makeDescLabel:(NSXMLNode *)node on:(NSView *)parent { NSString *text = nil; NSArray *children = [node children]; @@ -926,6 +1720,7 @@ make_desc_label (NSView *parent, NSXMLNode *node) rect.origin.x = rect.origin.y = 0; rect.size.width = 200; rect.size.height = 50; // sized later +# ifndef USE_IPHONE NSText *lab = [[NSText alloc] initWithFrame:rect]; [lab setEditable:NO]; [lab setDrawsBackground:NO]; @@ -936,236 +1731,328 @@ make_desc_label (NSView *parent, NSXMLNode *node) boldify (lab); [lab sizeToFit]; - place_child (parent, lab, NO); +# else // USE_IPHONE + +# ifndef USE_HTML_LABELS + + UILabel *lab = [self makeLabel:text]; + [lab setFont:[NSFont systemFontOfSize: [NSFont systemFontSize]]]; + hreffify (lab); + +# else // USE_HTML_LABELS + HTMLLabel *lab = [[HTMLLabel alloc] + initWithText:text + font:[NSFont systemFontOfSize: [NSFont systemFontSize]]]; + [lab setFrame:rect]; + [lab sizeToFit]; +# endif // USE_HTML_LABELS + + [self placeSeparator]; + +# endif // USE_IPHONE + + [self placeChild:lab on:parent]; [lab release]; } -static NSString * -unwrap (NSString *text) -{ - // 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 +/* Creates the NSTextField described by the given XML node. +*/ +- (void) makeTextField: (NSXMLNode *)node + on: (NSView *)parent + withLabel: (BOOL) label_p + horizontal: (BOOL) horiz_p +{ + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"", @"id", + @"", @"_label", + @"", @"arg", + nil]; + [self parseAttrs:dict node:node]; + NSString *label = [dict objectForKey:@"_label"]; + NSString *arg = [dict objectForKey:@"arg"]; - // 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--; + if (!label && label_p) { + NSAssert1 (0, @"no _label in %@", [node name]); + return; } - // skip leading blank lines in file - for (i = 0; i < nlines; i++) { - NSString *s = (NSString *) [lines objectAtIndex:i]; - if ([s length] > 0) - break; - } + NSAssert1 (arg, @"no arg in %@", label); - // 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; - } + NSRect rect; + rect.origin.x = rect.origin.y = 0; + rect.size.width = rect.size.height = 10; + + NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; + +# ifndef USE_IPHONE + + // 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:@""]; + +# else // USE_IPHONE + + txt.adjustsFontSizeToFitWidth = YES; + txt.textColor = [UIColor blackColor]; + txt.font = [UIFont systemFontOfSize: FONT_SIZE]; + txt.placeholder = @""; + txt.borderStyle = UITextBorderStyleRoundedRect; + txt.textAlignment = NSTextAlignmentRight; + txt.keyboardType = UIKeyboardTypeDefault; // Full kbd + txt.autocorrectionType = UITextAutocorrectionTypeNo; + txt.autocapitalizationType = UITextAutocapitalizationTypeNone; + txt.clearButtonMode = UITextFieldViewModeAlways; + txt.returnKeyType = UIReturnKeyDone; + txt.delegate = self; + txt.text = @""; + [txt setEnabled: YES]; + + rect.size.height = [txt.font lineHeight] * 1.2; + [txt setFrame:rect]; + +# endif // USE_IPHONE + + if (label) { + LABEL *lab = [self makeLabel:label]; + [self placeChild:lab on:parent]; + [lab release]; } - return text; + [self placeChild:txt on:parent right:(label ? YES : NO)]; + + [self bindSwitch:txt cmdline:arg]; + [txt release]; } -/* Converts any http: URLs in the given text field to clickable links. - */ -static void -hreffify (NSText *nstext) +/* Creates the NSTextField described by the given XML node, + and hooks it up to a Choose button and a file selector widget. +*/ +- (void) makeFileSelector: (NSXMLNode *)node + on: (NSView *)parent + dirsOnly: (BOOL) dirsOnly + withLabel: (BOOL) label_p + editable: (BOOL) editable_p { - NSString *text = [nstext string]; - [nstext setRichText:YES]; +# ifndef USE_IPHONE // No files. No selectors. + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"", @"id", + @"", @"_label", + @"", @"arg", + nil]; + [self parseAttrs:dict node:node]; + NSString *label = [dict objectForKey:@"_label"]; + NSString *arg = [dict objectForKey:@"arg"]; - 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) { + if (!label && label_p) { + NSAssert1 (0, @"no _label in %@", [node name]); + return; + } - // Find the beginning of a URL... - // - NSRange r2 = [text rangeOfString:@"http://" options:0 range:start]; - if (r2.location == NSNotFound) - break; + NSAssert1 (arg, @"no arg in %@", label); - // Next time around, start searching after this. - start.location = r2.location + r2.length; - start.length = L - start.location; + NSRect rect; + rect.origin.x = rect.origin.y = 0; + rect.size.width = rect.size.height = 10; + + NSTextField *txt = [[NSTextField alloc] initWithFrame:rect]; - // 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; + // make the default size be around 20 columns. + // + [txt setStringValue:@"123456789 123456789 "]; + [txt sizeToFit]; + [txt setSelectable:YES]; + [txt setEditable:editable_p]; + [txt setBezeled:editable_p]; + [txt setDrawsBackground:editable_p]; + [[txt cell] setWraps:NO]; + [[txt cell] setScrollable:YES]; + [[txt cell] setLineBreakMode:NSLineBreakByTruncatingHead]; + [txt setStringValue:@""]; - // Next time around, start searching after this. - start.location = r3.location; - start.length = L - start.location; + LABEL *lab = 0; + if (label) { + lab = [self makeLabel:label]; + [self placeChild:lab on:parent]; + [lab release]; + } - // Set r2 to the start/length of this URL. - r2.length = start.location - r2.location; + [self placeChild:txt on:parent right:(label ? YES : NO)]; - // Extract the URL. - NSString *nsurl = [text substringWithRange:r2]; - const char *url = [nsurl UTF8String]; + [self bindSwitch:txt cmdline:arg]; + [txt release]; + + // 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]; + } + + // 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]; - // Construct the RTF corresponding to url - // - const char *fmt = "{\\field{\\*\\fldinst{HYPERLINK \"%s\"}}%s}"; - char *rtf = malloc (strlen (fmt) + (strlen (url) * 2) + 10); - sprintf (rtf, fmt, url, url); - NSData *rtfdata = [NSData dataWithBytesNoCopy:rtf length:strlen(rtf)]; + [self placeChild:choose on:parent right:YES]; - // Insert the RTF into the NSText. - [nstext replaceCharactersInRange:r2 withRTF:rtfdata]; - } -} + // 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]; -/* Makes the first word of the text be bold. - */ -static void -boldify (NSText *nstext) -{ - NSString *text = [nstext string]; - NSRange r = [text rangeOfCharacterFromSet: - [NSCharacterSet whitespaceCharacterSet]]; - r.length = r.location; - r.location = 0; + [choose setTarget:[parent window]]; + if (dirsOnly) + [choose setAction:@selector(fileSelectorChooseDirsAction:)]; + else + [choose setAction:@selector(fileSelectorChooseAction:)]; - NSFont *font = [nstext font]; - font = [NSFont boldSystemFontOfSize:[font pointSize]]; - [nstext setFont:font range:r]; + [choose release]; +# endif // !USE_IPHONE } -static void layout_group (NSView *group, BOOL horiz_p); - +# ifndef USE_IPHONE -/* Creates an invisible NSBox (for layout purposes) to enclose the widgets - wrapped in

or in the XML. +/* Runs a modal file selector and sets the text field's value to the + selected file or directory. */ static void -make_group (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node, - BOOL horiz_p) +do_file_selector (NSTextField *txt, BOOL dirs_p) { - NSRect rect; - rect.size.width = rect.size.height = 1; - rect.origin.x = rect.origin.y = 0; - NSView *group = [[NSView alloc] initWithFrame:rect]; - traverse_children (prefs, opts, group, node); + NSOpenPanel *panel = [NSOpenPanel openPanel]; + [panel setAllowsMultipleSelection:NO]; + [panel setCanChooseFiles:!dirs_p]; + [panel setCanChooseDirectories:dirs_p]; - layout_group (group, horiz_p); + NSString *file = [txt stringValue]; + if ([file length] <= 0) { + file = NSHomeDirectory(); + if (dirs_p) + file = [file stringByAppendingPathComponent:@"Pictures"]; + } - rect.size.width = rect.size.height = 0; - NSBox *box = [[NSBox alloc] initWithFrame:rect]; - [box setTitlePosition:NSNoTitle]; - [box setBorderType:NSNoBorder]; - [box setContentViewMargins:rect.size]; - [box setContentView:group]; - [box sizeToFit]; +// 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]; - place_child (parent, box, NO); +#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 + } } -static void -layout_group (NSView *group, BOOL horiz_p) +/* Returns the NSTextField that is to the left of or above the NSButton. + */ +static NSTextField * +find_text_field_of_button (NSButton *button) { - NSArray *kids = [group subviews]; + NSView *parent = [button superview]; + NSArray *kids = [parent subviews]; int nkids = [kids count]; int i; - double maxx = 0, miny = 0; + NSTextField *f = 0; for (i = 0; i < nkids; i++) { - NSView *kid = [kids objectAtIndex:i]; - NSRect r = [kid frame]; - - if (horiz_p) { - maxx += r.size.width + COLUMN_SPACING; - if (r.size.height > -miny) miny = -r.size.height; - } else { - if (r.size.width > maxx) maxx = r.size.width; - miny = r.origin.y - r.size.height; + NSObject *kid = [kids objectAtIndex:i]; + if ([kid isKindOfClass:[NSTextField class]]) { + f = (NSTextField *) kid; + } else if (kid == button) { + if (! f) abort(); + return f; } } - - NSRect rect; - rect.size.width = maxx; - rect.size.height = -miny; - [group setFrame:rect]; + abort(); +} - double x = 0; - for (i = 0; i < nkids; i++) { - NSView *kid = [kids objectAtIndex:i]; - NSRect r = [kid frame]; - if (horiz_p) { - r.origin.y = rect.size.height - r.size.height; - r.origin.x = x; - x += r.size.width + COLUMN_SPACING; - } else { - r.origin.y -= miny; - } - [kid setFrame:r]; - } + +- (void) fileSelectorChooseAction:(NSObject *)arg +{ + NSButton *choose = (NSButton *) arg; + NSTextField *txt = find_text_field_of_button (choose); + do_file_selector (txt, NO); +} + +- (void) fileSelectorChooseDirsAction:(NSObject *)arg +{ + NSButton *choose = (NSButton *) arg; + NSTextField *txt = find_text_field_of_button (choose); + do_file_selector (txt, YES); } +#endif // !USE_IPHONE -static void -make_text_controls (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) + +- (void) makeTextLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent { +# ifndef USE_IPHONE /* Display Text: - (x) Computer Name and Time + (x) Computer name and time ( ) Text [__________________________] ( ) Text file [_________________] [Choose] ( ) URL [__________________________] + ( ) Shell Cmd [__________________________] textMode -text-mode date textMode -text-mode literal textLiteral -text-literal % textMode -text-mode file textFile -text-file % textMode -text-mode url textURL -text-url % + textMode -text-mode program textProgram -text-program % */ NSRect rect; rect.size.width = rect.size.height = 1; rect.origin.x = rect.origin.y = 0; - NSView *group = [[NSView alloc] initWithFrame:rect]; + NSView *group = [[NSView alloc] initWithFrame:rect]; NSView *rgroup = [[NSView alloc] initWithFrame:rect]; + Bool program_p = TRUE; + - NSXMLElement *node2; NSView *control; // This is how you link radio buttons together. @@ -1179,25 +2066,79 @@ make_text_controls (NSUserDefaultsController *prefs, initWithFrame:rect mode:NSRadioModeMatrix prototype:proto - numberOfRows:4 + numberOfRows: 4 + (program_p ? 1 : 0) numberOfColumns:1]; [matrix setAllowsEmptySelection:NO]; NSArrayController *cnames = [[NSArrayController alloc] initWithContent:nil]; - [cnames addObject:@"Computer Name and Time"]; + [cnames addObject:@"Computer name and time"]; [cnames addObject:@"Text"]; [cnames addObject:@"File"]; [cnames addObject:@"URL"]; + if (program_p) [cnames addObject:@"Shell Cmd"]; [matrix bind:@"content" toObject:cnames withKeyPath:@"arrangedObjects" options:nil]; [cnames release]; - bind_switch_to_preferences (prefs, matrix, @"-text-mode %", opts); + [self bindSwitch:matrix cmdline:@"-text-mode %"]; + + [self placeChild:matrix on:group]; + [self placeChild:rgroup on:group right:YES]; + + NSXMLNode *node2; + +# else // USE_IPHONE + + NSView *rgroup = parent; + NSXMLNode *node2; + + // + + node2 = [[NSXMLElement alloc] initWithName:@"select"]; + [node2 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"textMode", @"id", + nil]]; + + NSXMLNode *node3 = [[NSXMLElement alloc] initWithName:@"option"]; + [node3 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"date", @"id", + @"-text-mode date", @"arg-set", + @"Display the date and time", @"_label", + nil]]; + [node3 setParent: node2]; + //[node3 release]; + + node3 = [[NSXMLElement alloc] initWithName:@"option"]; + [node3 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"text", @"id", + @"-text-mode literal", @"arg-set", + @"Display static text", @"_label", + nil]]; + [node3 setParent: node2]; + //[node3 release]; + + node3 = [[NSXMLElement alloc] initWithName:@"option"]; + [node3 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"url", @"id", + @"Display the contents of a URL", @"_label", + nil]]; + [node3 setParent: node2]; + //[node3 release]; + + [self makeOptionMenu:node2 on:rgroup]; + +# endif // USE_IPHONE - place_child (group, matrix, NO); - place_child (group, rgroup, YES); // node2 = [[NSXMLElement alloc] initWithName:@"string"]; @@ -1205,11 +2146,19 @@ make_text_controls (NSUserDefaultsController *prefs, [NSDictionary dictionaryWithObjectsAndKeys: @"textLiteral", @"id", @"-text-literal %", @"arg", +# ifdef USE_IPHONE + @"Text to display", @"_label", +# endif nil]]; - make_text_field (prefs, opts, rgroup, node2, YES); - [node2 release]; + [self makeTextField:node2 on:rgroup +# ifndef USE_IPHONE + withLabel:NO +# else + withLabel:YES +# endif + horizontal:NO]; - rect = [last_child(rgroup) frame]; +// rect = [last_child(rgroup) frame]; /* // trying to make the text fields be enabled only when the checkbox is on.. control = last_child (rgroup); @@ -1220,6 +2169,7 @@ make_text_controls (NSUserDefaultsController *prefs, */ +# ifndef USE_IPHONE // node2 = [[NSXMLElement alloc] initWithName:@"string"]; [node2 setAttributesAsDictionary: @@ -1227,10 +2177,11 @@ make_text_controls (NSUserDefaultsController *prefs, @"textFile", @"id", @"-text-file %", @"arg", nil]]; - make_file_selector (prefs, opts, rgroup, node2, NO, YES); - [node2 release]; + [self makeFileSelector:node2 on:rgroup + dirsOnly:NO withLabel:NO editable:NO]; +# endif // !USE_IPHONE - rect = [last_child(rgroup) frame]; +// rect = [last_child(rgroup) frame]; // node2 = [[NSXMLElement alloc] initWithName:@"string"]; @@ -1238,11 +2189,33 @@ make_text_controls (NSUserDefaultsController *prefs, [NSDictionary dictionaryWithObjectsAndKeys: @"textURL", @"id", @"-text-url %", @"arg", +# ifdef USE_IPHONE + @"URL to display", @"_label", +# endif nil]]; - make_text_field (prefs, opts, rgroup, node2, YES); - [node2 release]; + [self makeTextField:node2 on:rgroup +# ifndef USE_IPHONE + withLabel:NO +# else + withLabel:YES +# endif + horizontal:NO]; + +// rect = [last_child(rgroup) frame]; + +# ifndef USE_IPHONE + if (program_p) { + // + node2 = [[NSXMLElement alloc] initWithName:@"string"]; + [node2 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"textProgram", @"id", + @"-text-program %", @"arg", + nil]]; + [self makeTextField:node2 on:rgroup withLabel:NO horizontal:NO]; + } - rect = [last_child(rgroup) frame]; +// rect = [last_child(rgroup) frame]; layout_group (rgroup, NO); @@ -1256,6 +2229,8 @@ make_text_controls (NSUserDefaultsController *prefs, control = last_child (rgroup); rect = [control frame]; rect.size.width = 30; // width of the string "Text", plus a bit... + if (program_p) + rect.size.width += 25; rect.size.height += LINE_SPACING; [matrix setCellSize:rect.size]; [matrix sizeToCells]; @@ -1270,7 +2245,7 @@ make_text_controls (NSUserDefaultsController *prefs, // the text fields. // rect.size = [matrix cellSize]; - rect.size.width *= 10; + rect.size.width = 300; [matrix setCellSize:rect.size]; [matrix sizeToCells]; @@ -1292,157 +2267,340 @@ make_text_controls (NSUserDefaultsController *prefs, [box setContentView:group]; [box sizeToFit]; - place_child (parent, box, NO); + [self placeChild:box on:parent]; + +# endif // !USE_IPHONE } -static void -make_image_controls (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) +- (void) makeImageLoaderControlBox:(NSXMLNode *)node on:(NSView *)parent { /* - [x] Grab Desktop Images - [ ] Choose Random Image: + [x] Grab desktop images + [ ] Choose random image: [__________________________] [Choose] - - */ NSXMLElement *node2; +# ifndef USE_IPHONE +# define SCREENS "Grab desktop images" +# define PHOTOS "Choose random images" +# else +# define SCREENS "Grab screenshots" +# define PHOTOS "Use photo library" +# endif + node2 = [[NSXMLElement alloc] initWithName:@"boolean"]; [node2 setAttributesAsDictionary: [NSDictionary dictionaryWithObjectsAndKeys: @"grabDesktopImages", @"id", - @"Grab Desktop Images", @"_label", + @ SCREENS, @"_label", @"-no-grab-desktop", @"arg-unset", nil]]; - make_checkbox (prefs, opts, parent, node2); - [node2 release]; + [self makeCheckbox:node2 on:parent]; node2 = [[NSXMLElement alloc] initWithName:@"boolean"]; [node2 setAttributesAsDictionary: [NSDictionary dictionaryWithObjectsAndKeys: @"chooseRandomImages", @"id", - @"Choose Random Images", @"_label", + @ PHOTOS, @"_label", @"-choose-random-images", @"arg-set", nil]]; - make_checkbox (prefs, opts, parent, node2); - [node2 release]; + [self makeCheckbox:node2 on:parent]; + + node2 = [[NSXMLElement alloc] initWithName:@"string"]; + [node2 setAttributesAsDictionary: + [NSDictionary dictionaryWithObjectsAndKeys: + @"imageDirectory", @"id", + @"Images from:", @"_label", + @"-image-directory %", @"arg", + nil]]; + [self makeFileSelector:node2 on:parent + dirsOnly:YES withLabel:YES editable:YES]; + +# undef SCREENS +# undef PHOTOS + +# ifndef USE_IPHONE + // Add a second, explanatory label below the file/URL selector. + + LABEL *lab2 = 0; + lab2 = [self makeLabel:@"(Local folder, or URL of RSS or Atom feed)"]; + [self placeChild:lab2 on:parent]; + + // Pack it in a little tighter vertically. + NSRect r2 = [lab2 frame]; + r2.origin.x += 20; + r2.origin.y += 14; + [lab2 setFrameOrigin:r2.origin]; + [lab2 release]; +# endif // USE_IPHONE +} + + +#pragma mark Layout for controls + + +# ifndef USE_IPHONE +static NSView * +last_child (NSView *parent) +{ + NSArray *kids = [parent subviews]; + int nkids = [kids count]; + if (nkids == 0) + return 0; + else + return [kids objectAtIndex:nkids-1]; +} +#endif // USE_IPHONE + + +/* 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. + */ +- (void) placeChild: +# ifdef USE_IPHONE + (NSObject *)child +# else + (NSView *)child +# endif + on:(NSView *)parent right:(BOOL)right_p +{ +# ifndef USE_IPHONE + 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; + } + NSRect r = [child frame]; + r.origin = rect.origin; + [child setFrame:r]; + [parent addSubview:child]; + +# else // USE_IPHONE + + // Controls is an array of arrays of the controls, divided into sections. + if (! controls) + controls = [[NSMutableArray arrayWithCapacity:10] retain]; + if ([controls count] == 0) + [controls addObject: [NSMutableArray arrayWithCapacity:10]]; + NSMutableArray *current = [controls objectAtIndex:[controls count]-1]; + + if (!right_p || [current count] == 0) { + // Nothing on the current line. Add this object. + [current addObject: child]; + } else { + // Something's on the current line already. + NSObject *old = [current objectAtIndex:[current count]-1]; + if ([old isKindOfClass:[NSMutableArray class]]) { + // Already an array in this cell. Append. + NSAssert ([(NSArray *) old count] < 4, @"internal error"); + [(NSMutableArray *) old addObject: child]; + } else { + // Replace the control in this cell with an array, then app + NSMutableArray *a = [NSMutableArray arrayWithObjects: old, child, nil]; + [current replaceObjectAtIndex:[current count]-1 withObject:a]; + } + } +# endif // USE_IPHONE +} + + +- (void) placeChild:(NSView *)child on:(NSView *)parent +{ + [self placeChild:child on:parent right:NO]; +} + + +#ifdef USE_IPHONE + +// Start putting subsequent children in a new group, to create a new +// section on the UITableView. +// +- (void) placeSeparator +{ + if (! controls) return; + if ([controls count] == 0) return; + if ([[controls objectAtIndex:[controls count]-1] + count] > 0) + [controls addObject: [NSMutableArray arrayWithCapacity:10]]; +} +#endif // USE_IPHONE + + + +/* Creates an invisible NSBox (for layout purposes) to enclose the widgets + wrapped in
or in the XML. + */ +- (void) makeGroup:(NSXMLNode *)node + on:(NSView *)parent + horizontal:(BOOL) horiz_p +{ +# ifdef USE_IPHONE + if (!horiz_p) [self placeSeparator]; + [self traverseChildren:node on:parent]; + if (!horiz_p) [self placeSeparator]; +# else // !USE_IPHONE + NSRect rect; + rect.size.width = rect.size.height = 1; + rect.origin.x = rect.origin.y = 0; + NSView *group = [[NSView alloc] initWithFrame:rect]; + [self traverseChildren:node on:group]; + + layout_group (group, horiz_p); + + rect.size.width = rect.size.height = 0; + NSBox *box = [[NSBox alloc] initWithFrame:rect]; + [box setTitlePosition:NSNoTitle]; + [box setBorderType:NSNoBorder]; + [box setContentViewMargins:rect.size]; + [box setContentView:group]; + [box sizeToFit]; - node2 = [[NSXMLElement alloc] initWithName:@"string"]; - [node2 setAttributesAsDictionary: - [NSDictionary dictionaryWithObjectsAndKeys: - @"imageDirectory", @"id", - @"Images Directory:", @"_label", - @"-image-directory %", @"arg", - nil]]; - make_file_selector (prefs, opts, parent, node2, YES, NO); - [node2 release]; + [self placeChild:box on:parent]; +# endif // !USE_IPHONE } +#ifndef USE_IPHONE +static void +layout_group (NSView *group, BOOL horiz_p) +{ + NSArray *kids = [group subviews]; + int nkids = [kids count]; + int i; + double maxx = 0, miny = 0; + for (i = 0; i < nkids; i++) { + NSView *kid = [kids objectAtIndex:i]; + NSRect r = [kid frame]; + + if (horiz_p) { + maxx += r.size.width + COLUMN_SPACING; + if (r.size.height > -miny) miny = -r.size.height; + } else { + if (r.size.width > maxx) maxx = r.size.width; + miny = r.origin.y - r.size.height; + } + } + + NSRect rect; + rect.origin.x = 0; + rect.origin.y = 0; + rect.size.width = maxx; + rect.size.height = -miny; + [group setFrame:rect]; + + double x = 0; + for (i = 0; i < nkids; i++) { + NSView *kid = [kids objectAtIndex:i]; + NSRect r = [kid frame]; + if (horiz_p) { + r.origin.y = rect.size.height - r.size.height; + r.origin.x = x; + x += r.size.width + COLUMN_SPACING; + } else { + r.origin.y -= miny; + } + [kid setFrame:r]; + } +} +#endif // !USE_IPHONE + /* Create some kind of control corresponding to the given XML node. */ -static void -make_control (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, NSView *parent, NSXMLNode *node) +-(void)makeControl:(NSXMLNode *)node on:(NSView *)parent { NSString *name = [node name]; if ([node kind] == NSXMLCommentKind) return; + + if ([node kind] == NSXMLTextKind) { + NSString *s = [(NSString *) [node objectValue] + stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (! [s isEqualToString:@""]) { + NSAssert1 (0, @"unexpected text: %@", s); + } + return; + } + if ([node kind] != NSXMLElementKind) { - NSAssert2 (0, @"weird XML node kind: %d: %@", [node kind], node); + NSAssert2 (0, @"weird XML node kind: %d: %@", (int)[node kind], node); return; } if ([name isEqualToString:@"hgroup"] || [name isEqualToString:@"vgroup"]) { - BOOL horiz_p = [name isEqualToString:@"hgroup"]; - make_group (prefs, opts, parent, node, horiz_p); + [self makeGroup:node on:parent + horizontal:[name isEqualToString:@"hgroup"]]; } else if ([name isEqualToString:@"command"]) { // do nothing: this is the "-root" business } else if ([name isEqualToString:@"boolean"]) { - make_checkbox (prefs, opts, parent, node); + [self makeCheckbox:node on:parent]; } else if ([name isEqualToString:@"string"]) { - make_text_field (prefs, opts, parent, node, NO); + [self makeTextField:node on:parent withLabel:NO horizontal:NO]; } else if ([name isEqualToString:@"file"]) { - make_file_selector (prefs, opts, parent, node, NO, NO); + [self makeFileSelector:node on:parent + dirsOnly:NO withLabel:YES editable:NO]; } else if ([name isEqualToString:@"number"]) { - make_number_selector (prefs, opts, parent, node); + [self makeNumberSelector:node on:parent]; } else if ([name isEqualToString:@"select"]) { - make_option_menu (prefs, opts, parent, node); + [self makeOptionMenu:node on:parent]; } else if ([name isEqualToString:@"_description"]) { - make_desc_label (parent, node); + [self makeDescLabel:node on:parent]; } else if ([name isEqualToString:@"xscreensaver-text"]) { - make_text_controls (prefs, opts, parent, node); + [self makeTextLoaderControlBox:node on:parent]; } else if ([name isEqualToString:@"xscreensaver-image"]) { - make_image_controls (prefs, opts, parent, node); + [self makeImageLoaderControlBox:node on:parent]; } else { - NSAssert1 (0, @"unknown tag: %@\n", name); + NSAssert1 (0, @"unknown tag: %@", name); } } /* Iterate over and process the children of this XML node. */ -static void -traverse_children (NSUserDefaultsController *prefs, - const XrmOptionDescRec *opts, - NSView *parent, NSXMLNode *node) +- (void)traverseChildren:(NSXMLNode *)node on:(NSView *)parent { NSArray *children = [node children]; int i, count = [children count]; for (i = 0; i < count; i++) { NSXMLNode *child = [children objectAtIndex:i]; - make_control (prefs, opts, parent, child); + [self makeControl:child on:parent]; } } -/* Handle the options on the top level tag. - */ -static void -parse_xscreensaver_tag (NSXMLNode *node) -{ - NSMutableDictionary *dict = - [NSMutableDictionary dictionaryWithObjectsAndKeys: - @"", @"name", - @"", @"_label", - nil]; - parse_attrs (dict, node); - NSString *name = [dict objectForKey:@"name"]; - NSString *label = [dict objectForKey:@"_label"]; - - if (!label) { - NSAssert1 (0, @"no _label in %@\n", [node name]); - return; - } - if (!name) { - NSAssert1 (0, @"no name in \"%@\"\n", label); - return; - } - - // #### do any callers need the "name" field for anything? -} +# ifndef USE_IPHONE /* Kludgey magic to make the window enclose the controls we created. */ @@ -1452,8 +2610,7 @@ fix_contentview_size (NSView *parent) NSRect f; NSArray *kids = [parent subviews]; int nkids = [kids count]; - NSView *text; // the NSText at the bottom of the window - NSView *last; // the last child before the NSText + NSView *text = 0; // the NSText at the bottom of the window double maxx = 0, miny = 0; int i; @@ -1469,17 +2626,17 @@ fix_contentview_size (NSView *parent) f = [kid frame]; if (f.origin.x + f.size.width > maxx) maxx = f.origin.x + f.size.width; if (f.origin.y - f.size.height < miny) miny = f.origin.y; - last = kid; // NSLog(@"start: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@", // f.size.width, f.size.height, f.origin.x, f.origin.y, // f.origin.y + f.size.height, [kid class]); } - if (maxx < 350) maxx = 350; // leave room for the NSText paragraph... + if (maxx < 400) maxx = 400; // leave room for the NSText paragraph... /* Now that we know the width of the window, set the width of the NSText to that, so that it can decide what its height needs to be. */ + if (! text) abort(); f = [text frame]; // NSLog(@"text old: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@", // f.size.width, f.size.height, f.origin.x, f.origin.y, @@ -1495,10 +2652,33 @@ fix_contentview_size (NSView *parent) miny = f.origin.y - LINE_SPACING; [text setFrame:f]; - // Stop second-guessing us on sizing now. Size is now locked. + // Lock the width of the field and unlock the height, and let it resize + // once more, to compute the proper height of the text for that width. + // [(NSText *) text setHorizontallyResizable:NO]; + [(NSText *) text setVerticallyResizable:YES]; + [(NSText *) text sizeToFit]; + + // Now lock the height too: no more resizing this text field. + // [(NSText *) text setVerticallyResizable:NO]; - + + // Now reposition the top edge of the text field to be back where it + // was before we changed the height. + // + float oh = f.size.height; + f = [text frame]; + float dh = f.size.height - oh; + f.origin.y += dh; + + // #### This is needed in OSX 10.5, but is wrong in OSX 10.6. WTF?? + // If we do this in 10.6, the text field moves down, off the window. + // So instead we repair it at the end, at the "WTF2" comment. + [text setFrame:f]; + + // Also adjust the parent height by the change in height of the text field. + miny -= dh; + // NSLog(@"text new: %3.0f x %3.0f @ %3.0f %3.0f %3.0f %@", // f.size.width, f.size.height, f.origin.x, f.origin.y, // f.origin.y + f.size.height, [text class]); @@ -1507,10 +2687,10 @@ fix_contentview_size (NSView *parent) /* Set the contentView to the size of the children. */ f = [parent frame]; - float yoff = f.size.height; +// float yoff = f.size.height; f.size.width = maxx + LEFT_MARGIN; f.size.height = -(miny - LEFT_MARGIN*2); - yoff = f.size.height - yoff; +// yoff = f.size.height - yoff; [parent setFrame:f]; // NSLog(@"max: %3.0f x %3.0f @ %3.0f %3.0f", @@ -1531,6 +2711,25 @@ fix_contentview_size (NSView *parent) // f.origin.y + f.size.height, [kid class]); } +/* +Bad: + parent: 420 x 541 @ 0 0 + text: 380 x 100 @ 20 22 miny=-501 + +Good: + parent: 420 x 541 @ 0 0 + text: 380 x 100 @ 20 50 miny=-501 +*/ + + // #### WTF2: See "WTF" above. If the text field is off the screen, + // move it up. We need this on 10.6 but not on 10.5. Auugh. + // + f = [text frame]; + if (f.origin.y < 50) { // magic numbers, yay + f.origin.y = 50; + [text setFrame:f]; + } + /* Set the kids to track the top left corner of the window when resized. Set the NSText to track the bottom right corner as well. */ @@ -1542,29 +2741,11 @@ fix_contentview_size (NSView *parent) [kid setAutoresizingMask:mask]; } } +# endif // !USE_IPHONE -- (void) okClicked:(NSObject *)arg -{ - [userDefaultsController commitEditing]; - [userDefaultsController save:self]; - [NSApp endSheet:self returnCode:NSOKButton]; - [self close]; -} - -- (void) cancelClicked:(NSObject *)arg -{ - [userDefaultsController revert:self]; - [NSApp endSheet:self returnCode:NSCancelButton]; - [self close]; -} - -- (void) resetClicked:(NSObject *)arg -{ - [userDefaultsController revertToInitialValues:self]; -} - +#ifndef USE_IPHONE static NSView * wrap_with_buttons (NSWindow *window, NSView *panel) { @@ -1634,7 +2815,26 @@ wrap_with_buttons (NSWindow *window, NSView *panel) rect.origin.y += rect.size.height; NSBox *pbox = [[NSBox alloc] initWithFrame:rect]; [pbox setTitlePosition:NSNoTitle]; - [pbox setBorderType:NSNoBorder]; + [pbox setBorderType:NSBezelBorder]; + + // Enforce a max height on the dialog, so that it's obvious to me + // (on a big screen) when the dialog will fall off the bottom of + // a small screen (e.g., 1024x768 laptop with a huge bottom dock). + { + NSRect f = [panel frame]; + int screen_height = (768 // shortest "modern" Mac display + - 22 // menu bar + - 56 // System Preferences toolbar + - 140 // default magnified bottom dock icon + ); + if (f.size.height > screen_height) { + NSLog(@"%@ height was %.0f; clipping to %d", + [panel class], f.size.height, screen_height); + f.size.height = screen_height; + [panel setFrame:f]; + } + } + [pbox addSubview:panel]; [pbox addSubview:bbox]; [pbox sizeToFit]; @@ -1649,46 +2849,569 @@ wrap_with_buttons (NSWindow *window, NSView *panel) [ok setTarget:window]; [cancel setTarget:window]; [reset setTarget:window]; - [ok setAction:@selector(okClicked:)]; - [cancel setAction:@selector(cancelClicked:)]; - [reset setAction:@selector(resetClicked:)]; + [ok setAction:@selector(okAction:)]; + [cancel setAction:@selector(cancelAction:)]; + [reset setAction:@selector(resetAction:)]; return pbox; } +#endif // !USE_IPHONE /* Iterate over and process the children of the root node of the XML document. */ -static void -traverse_tree (NSUserDefaultsController *prefs, - NSWindow *window, const XrmOptionDescRec *opts, NSXMLNode *node) +- (void)traverseTree { +# ifdef USE_IPHONE + NSView *parent = [self view]; +# else + NSWindow *parent = self; +#endif + NSXMLNode *node = xml_root; + if (![[node name] isEqualToString:@"screensaver"]) { NSAssert (0, @"top level node is not "); } - parse_xscreensaver_tag (node); + saver_name = [self parseXScreenSaverTag: node]; + saver_name = [saver_name stringByReplacingOccurrencesOfString:@" " + withString:@""]; + [saver_name retain]; +# ifndef USE_IPHONE + NSRect rect; rect.origin.x = rect.origin.y = 0; rect.size.width = rect.size.height = 1; NSView *panel = [[NSView alloc] initWithFrame:rect]; - - traverse_children (prefs, opts, panel, node); + [self traverseChildren:node on:panel]; fix_contentview_size (panel); - NSView *root = wrap_with_buttons (window, panel); - [prefs setAppliesImmediately:NO]; + NSView *root = wrap_with_buttons (parent, panel); + [userDefaultsController setAppliesImmediately:NO]; [panel setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; - rect = [window frameRectForContentRect:[root frame]]; - [window setFrame:rect display:NO]; - [window setMinSize:rect.size]; + rect = [parent frameRectForContentRect:[root frame]]; + [parent setFrame:rect display:NO]; + [parent setMinSize:rect.size]; - [window setContentView:root]; + [parent setContentView:root]; + +# else // USE_IPHONE + + CGRect r = [parent frame]; + r.size = [[UIScreen mainScreen] bounds].size; + [parent setFrame:r]; + [self traverseChildren:node on:parent]; + +# endif // USE_IPHONE +} + + +- (void)parser:(NSXMLParser *)parser + didStartElement:(NSString *)elt + namespaceURI:(NSString *)ns + qualifiedName:(NSString *)qn + attributes:(NSDictionary *)attrs +{ + NSXMLElement *e = [[NSXMLElement alloc] initWithName:elt]; + [e setKind:SimpleXMLElementKind]; + [e setAttributesAsDictionary:attrs]; + NSXMLElement *p = xml_parsing; + [e setParent:p]; + xml_parsing = e; + if (! xml_root) + xml_root = xml_parsing; +} + +- (void)parser:(NSXMLParser *)parser + didEndElement:(NSString *)elt + namespaceURI:(NSString *)ns + qualifiedName:(NSString *)qn +{ + NSXMLElement *p = xml_parsing; + if (! p) { + NSLog(@"extra close: %@", elt); + } else if (![[p name] isEqualToString:elt]) { + NSLog(@"%@ closed by %@", [p name], elt); + } else { + NSXMLElement *n = xml_parsing; + xml_parsing = [n parent]; + } +} + + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string +{ + NSXMLElement *e = [[NSXMLElement alloc] initWithName:@"text"]; + [e setKind:SimpleXMLTextKind]; + NSXMLElement *p = xml_parsing; + [e setParent:p]; + [e setObjectValue: string]; +} + + +# ifdef USE_IPHONE +# ifdef USE_PICKER_VIEW + +#pragma mark UIPickerView delegate methods + +- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pv +{ + return 1; // Columns +} + +- (NSInteger)pickerView:(UIPickerView *)pv + numberOfRowsInComponent:(NSInteger)column +{ + NSAssert (column == 0, @"weird column"); + NSArray *a = [picker_values objectAtIndex: [pv tag]]; + if (! a) return 0; // Too early? + return [a count]; +} + +- (CGFloat)pickerView:(UIPickerView *)pv + rowHeightForComponent:(NSInteger)column +{ + return FONT_SIZE; +} + +- (CGFloat)pickerView:(UIPickerView *)pv + widthForComponent:(NSInteger)column +{ + NSAssert (column == 0, @"weird column"); + NSArray *a = [picker_values objectAtIndex: [pv tag]]; + if (! a) return 0; // Too early? + + UIFont *f = [UIFont systemFontOfSize:[NSFont systemFontSize]]; + CGFloat max = 0; + for (NSArray *a2 in a) { + NSString *s = [a2 objectAtIndex:0]; + CGSize r = [s sizeWithFont:f]; + if (r.width > max) max = r.width; + } + + max *= 1.7; // WTF!! + + if (max > 320) + max = 320; + else if (max < 120) + max = 120; + + return max; + +} + + +- (NSString *)pickerView:(UIPickerView *)pv + titleForRow:(NSInteger)row + forComponent:(NSInteger)column +{ + NSAssert (column == 0, @"weird column"); + NSArray *a = [picker_values objectAtIndex: [pv tag]]; + if (! a) return 0; // Too early? + a = [a objectAtIndex:row]; + NSAssert (a, @"internal error"); + return [a objectAtIndex:0]; +} + +# endif // USE_PICKER_VIEW + + +#pragma mark UITableView delegate methods + +- (void) addResetButton +{ + [[self navigationItem] + setRightBarButtonItem: [[UIBarButtonItem alloc] + initWithTitle: @"Reset to Defaults" + style: UIBarButtonItemStyleBordered + target:self + action:@selector(resetAction:)]]; + NSString *s = saver_name; + if ([self view].frame.size.width > 320) + s = [s stringByAppendingString: @" Settings"]; + [self navigationItem].title = s; +} + + +- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o +{ + return YES; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tv { + // Number of vertically-stacked white boxes. + return [controls count]; +} + +- (NSInteger)tableView:(UITableView *)tableView + numberOfRowsInSection:(NSInteger)section +{ + // Number of lines in each vertically-stacked white box. + NSAssert (controls, @"internal error"); + return [[controls objectAtIndex:section] count]; +} + +- (NSString *)tableView:(UITableView *)tv + titleForHeaderInSection:(NSInteger)section +{ + // Titles above each vertically-stacked white box. +// if (section == 0) +// return [saver_name stringByAppendingString:@" Settings"]; + return nil; +} + + +- (CGFloat)tableView:(UITableView *)tv + heightForRowAtIndexPath:(NSIndexPath *)ip +{ + CGFloat h = [tv rowHeight]; + + NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] + objectAtIndex:[ip indexAtPosition:1]]; + + if ([ctl isKindOfClass:[NSArray class]]) { + NSArray *set = (NSArray *) ctl; + switch ([set count]) { + case 4: +# ifdef LABEL_ABOVE_SLIDER + h *= 1.7; break; // label + left/slider/right: 2 1/2 lines +# endif + case 3: h *= 1.2; break; // left/slider/right: 1 1/2 lines + case 2: + if ([[set objectAtIndex:1] isKindOfClass:[UITextField class]]) + h *= 1.2; + break; + } + } else if ([ctl isKindOfClass:[UILabel class]]) { + UILabel *t = (UILabel *) ctl; + CGRect r = t.frame; + r.size.width = 250; // WTF! Black magic! + r.size.width -= LEFT_MARGIN; + [t setFrame:r]; + [t sizeToFit]; + r = t.frame; + h = r.size.height + LINE_SPACING * 3; +# ifdef USE_HTML_LABELS + + } else if ([ctl isKindOfClass:[HTMLLabel class]]) { + + HTMLLabel *t = (HTMLLabel *) ctl; + CGRect r = t.frame; + r.size.width = [tv frame].size.width; + r.size.width -= LEFT_MARGIN * 2; + [t setFrame:r]; + [t sizeToFit]; + r = t.frame; + h = r.size.height + LINE_SPACING * 3; + +# endif // USE_HTML_LABELS + } else { + CGFloat h2 = [ctl frame].size.height; + h2 += LINE_SPACING * 2; + if (h2 > h) h = h2; + } + + return h; +} + + +- (void)refreshTableView +{ + UITableView *tv = (UITableView *) [self view]; + NSMutableArray *a = [NSMutableArray arrayWithCapacity:20]; + int rows = [self numberOfSectionsInTableView:tv]; + for (int i = 0; i < rows; i++) { + int cols = [self tableView:tv numberOfRowsInSection:i]; + for (int j = 0; j < cols; j++) { + NSUInteger ip[2]; + ip[0] = i; + ip[1] = j; + [a addObject: [NSIndexPath indexPathWithIndexes:ip length:2]]; + } + } + + [tv beginUpdates]; + [tv reloadRowsAtIndexPaths:a withRowAnimation:UITableViewRowAnimationNone]; + [tv endUpdates]; +} + + +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)o +{ + [NSTimer scheduledTimerWithTimeInterval: 0 + target:self + selector:@selector(refreshTableView) + userInfo:nil + repeats:NO]; +} + + +#ifndef USE_PICKER_VIEW + +- (void)updateRadioGroupCell:(UITableViewCell *)cell + button:(RadioButton *)b +{ + NSArray *item = [[b items] objectAtIndex: [b index]]; + NSString *pref_key = [item objectAtIndex:1]; + NSObject *pref_val = [item objectAtIndex:2]; + NSObject *current = [userDefaultsController objectForKey:pref_key]; + + // Convert them both to strings and compare those, so that + // we don't get screwed by int 1 versus string "1". + // Will boolean true/1 screw us here too? + // + NSString *pref_str = ([pref_val isKindOfClass:[NSString class]] + ? (NSString *) pref_val + : [(NSNumber *) pref_val stringValue]); + NSString *current_str = ([current isKindOfClass:[NSString class]] + ? (NSString *) current + : [(NSNumber *) current stringValue]); + BOOL match_p = [current_str isEqualToString:pref_str]; + + // NSLog(@"\"%@\" = \"%@\" | \"%@\" ", pref_key, pref_val, current_str); + + if (match_p) + [cell setAccessoryType:UITableViewCellAccessoryCheckmark]; + else + [cell setAccessoryType:UITableViewCellAccessoryNone]; +} + + +- (void)tableView:(UITableView *)tv + didSelectRowAtIndexPath:(NSIndexPath *)ip +{ + RadioButton *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] + objectAtIndex:[ip indexAtPosition:1]]; + if (! [ctl isKindOfClass:[RadioButton class]]) + return; + + [self radioAction:ctl]; + [self refreshTableView]; +} + + +#endif // !USE_PICKER_VIEW + + + +- (UITableViewCell *)tableView:(UITableView *)tv + cellForRowAtIndexPath:(NSIndexPath *)ip +{ +#if 0 + /* #### If we re-use cells, then clicking on a checkbox RadioButton + (in non-USE_PICKER_VIEW mode) makes all the cells disappear. + This doesn't happen if we don't re-use any cells. Oh well. + */ + NSString *id = [NSString stringWithFormat: @"%d:%d", + [ip indexAtPosition:0], + [ip indexAtPosition:1]]; + UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier: id]; + + if (cell) return cell; +#else + NSString *id = nil; + UITableViewCell *cell; +#endif + + cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault + reuseIdentifier: id] + autorelease]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + CGRect p = [cell frame]; + CGRect r; + + p.size.height = [self tableView:tv heightForRowAtIndexPath:ip]; + [cell setFrame:p]; + + // Allocate more space to the controls on iPad screens, + // and on landscape-mode iPhones. + CGFloat ww = [tv frame].size.width; + CGFloat left_edge = (ww > 700 + ? p.size.width * 0.9 + : ww > 320 + ? p.size.width * 0.5 + : p.size.width * 0.3); + CGFloat right_edge = p.origin.x + p.size.width - LEFT_MARGIN; + + + NSView *ctl = [[controls objectAtIndex:[ip indexAtPosition:0]] + objectAtIndex:[ip indexAtPosition:1]]; + + if ([ctl isKindOfClass:[NSArray class]]) { + // This cell has a set of objects in it. + NSArray *set = (NSArray *) ctl; + switch ([set count]) { + case 2: + { + // With 2 elements, the first of the pair must be a label. + UILabel *label = (UILabel *) [set objectAtIndex: 0]; + NSAssert ([label isKindOfClass:[UILabel class]], @"unhandled type"); + ctl = [set objectAtIndex: 1]; + + r = [ctl frame]; + if ([ctl isKindOfClass:[UISwitch class]]) { + // Flush right checkboxes. + ctl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + r.size.width = 80; // Magic. + r.origin.x = right_edge - r.size.width; + } else { + // Expandable sliders. + ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth; + r.origin.x = left_edge; + r.size.width = right_edge - r.origin.x; + } + r.origin.y = (p.size.height - r.size.height) / 2; + [ctl setFrame:r]; + + // Make a box. + NSView *box = [[UIView alloc] initWithFrame:p]; + [box addSubview: ctl]; + + // cell.textLabel.text = [(UILabel *) ctl text]; + r = [label frame]; + r.origin.x = LEFT_MARGIN; + r.origin.y = 0; + r.size.width = [ctl frame].origin.x - r.origin.x; + r.size.height = p.size.height; + [label setFrame:r]; + [label setFont:[NSFont boldSystemFontOfSize: FONT_SIZE]]; + label.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + box. autoresizingMask = UIViewAutoresizingFlexibleWidth; + [box addSubview: label]; + + ctl = box; + } + break; + case 3: + case 4: + { + // With 3 elements, the first and last must be labels. + // With 4 elements, the first, second and last must be labels. + int i = 0; + UILabel *top = ([set count] == 4 + ? [set objectAtIndex: i++] + : 0); + UILabel *left = [set objectAtIndex: i++]; + NSView *mid = [set objectAtIndex: i++]; + UILabel *right = [set objectAtIndex: i++]; + NSAssert (!top || [top isKindOfClass:[UILabel class]], @"WTF"); + NSAssert ( [left isKindOfClass:[UILabel class]], @"WTF"); + NSAssert ( ![mid isKindOfClass:[UILabel class]], @"WTF"); + NSAssert ( [right isKindOfClass:[UILabel class]], @"WTF"); + + // 3 elements: control at top of cell. + // 4 elements: center the control vertically. + r = [mid frame]; +# ifdef LABEL_ABOVE_SLIDER + left_edge = LEFT_MARGIN; +# endif + r.origin.x = left_edge; + r.size.width = right_edge - r.origin.x; + r.origin.y = ([set count] == 3 + ? 8 + : (p.size.height - r.size.height) / 2); + [mid setFrame:r]; + + // Top label goes above, flush center/top. + if (top) { + r.size = [[top text] sizeWithFont:[top font] + constrainedToSize: + CGSizeMake (p.size.width - LEFT_MARGIN*2, + 100000) + lineBreakMode:[top lineBreakMode]]; + r.origin.x = (p.size.width - r.size.width) / 2; + r.origin.y = 4; + [top setFrame:r]; + } + + // Left label goes under control, flush left/bottom. + r.size = [[left text] sizeWithFont:[left font] + constrainedToSize: + CGSizeMake(p.size.width - LEFT_MARGIN*2, + 100000) + lineBreakMode:[left lineBreakMode]]; + r.origin.x = [mid frame].origin.x; + r.origin.y = p.size.height - r.size.height - 4; + [left setFrame:r]; + + // Right label goes under control, flush right/bottom. + r = [right frame]; + r.size = [[right text] sizeWithFont:[right font] + constrainedToSize: + CGSizeMake(p.size.width - LEFT_MARGIN*2, + 1000000) + lineBreakMode:[right lineBreakMode]]; + r.origin.x = ([mid frame].origin.x + [mid frame].size.width - + r.size.width); + r.origin.y = [left frame].origin.y; + [right setFrame:r]; + + // Then make a box. + ctl = [[UIView alloc] initWithFrame:p]; + if (top) { +# ifdef LABEL_ABOVE_SLIDER + [ctl addSubview: top]; + top.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin| + UIViewAutoresizingFlexibleRightMargin); +# else + r = [top frame]; + r.origin.x = LEFT_MARGIN; + r.origin.y = 0; + r.size.width = [mid frame].origin.x - r.origin.x; + r.size.height = p.size.height; + [top setFrame:r]; + top.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + [ctl addSubview: top]; +# endif + } + [ctl addSubview: left]; + [ctl addSubview: mid]; + [ctl addSubview: right]; + + left. autoresizingMask = UIViewAutoresizingFlexibleRightMargin; + mid. autoresizingMask = UIViewAutoresizingFlexibleWidth; + right.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + ctl. autoresizingMask = UIViewAutoresizingFlexibleWidth; + } + break; + default: + NSAssert (0, @"unhandled size"); + } + } else { + // A single view, not a pair. + + r = [ctl frame]; + r.origin.x = LEFT_MARGIN; + r.origin.y = LINE_SPACING; + r.size.width = right_edge - r.origin.x; + [ctl setFrame:r]; + + ctl.autoresizingMask = UIViewAutoresizingFlexibleWidth; + +# ifndef USE_PICKER_VIEW + if ([ctl isKindOfClass:[RadioButton class]]) + [self updateRadioGroupCell:cell button:(RadioButton *)ctl]; +# endif // USE_PICKER_VIEW + } + + if ([ctl isKindOfClass:[UILabel class]]) { + // Make label full height to allow text to line-wrap if necessary. + r = [ctl frame]; + r.origin.y = p.origin.y; + r.size.height = p.size.height; + [ctl setFrame:r]; + } + + [cell.contentView addSubview: ctl]; + + return cell; } +# endif // USE_IPHONE /* When this object is instantiated, it parses the XML file and creates @@ -1696,44 +3419,67 @@ traverse_tree (NSUserDefaultsController *prefs, The default size of the view is just big enough to hold them all. */ - (id)initWithXMLFile: (NSString *) xml_file - options: (const XrmOptionDescRec *) opts - controller: (NSUserDefaultsController *) prefs + options: (const XrmOptionDescRec *) _opts + controller: (NSUserDefaultsController *) _prefs + defaults: (NSDictionary *) _defs { - if (! (self = [super init])) - return 0; - - // instance variable - userDefaultsController = prefs; - [prefs retain]; +# ifndef USE_IPHONE + self = [super init]; +# else // !USE_IPHONE + self = [super initWithStyle:UITableViewStyleGrouped]; + self.title = [saver_name stringByAppendingString:@" Settings"]; +# endif // !USE_IPHONE + if (! self) return 0; + + // instance variables + opts = _opts; + defaultOptions = _defs; + userDefaultsController = _prefs; + [userDefaultsController retain]; NSURL *furl = [NSURL fileURLWithPath:xml_file]; if (!furl) { - NSAssert1 (0, @"can't URLify \"%@\"\n", xml_file); + NSAssert1 (0, @"can't URLify \"%@\"", xml_file); return nil; } +#if 0 // -- the old way NSError *err = nil; NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl options:(NSXMLNodePreserveWhitespace | NSXMLNodePreserveCDATA) error:&err]; -/* clean up? - if (!xmlDoc) { - xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl - options:NSXMLDocumentTidyXML - error:&err]; - } -*/ if (!xmlDoc || err) { if (err) - NSAssert2 (0, @"XML Error: %@: %@\n", + NSAssert2 (0, @"XML Error: %@: %@", xml_file, [err localizedDescription]); return nil; } traverse_tree (prefs, self, opts, [xmlDoc rootElement]); +#endif /* 0 */ + + + NSXMLParser *xmlDoc = [[NSXMLParser alloc] initWithContentsOfURL:furl]; + if (!xmlDoc) { + NSAssert1 (0, @"XML Error: %@", xml_file); + return nil; + } + [xmlDoc setDelegate:self]; + if (! [xmlDoc parse]) { + NSError *err = [xmlDoc parserError]; + NSAssert2 (0, @"XML Error: %@: %@", xml_file, err); + return nil; + } + + [self traverseTree]; + xml_root = 0; + +# ifdef USE_IPHONE + [self addResetButton]; +# endif return self; } @@ -1741,7 +3487,16 @@ traverse_tree (NSUserDefaultsController *prefs, - (void) dealloc { + [saver_name release]; [userDefaultsController release]; +# ifdef USE_IPHONE + [controls release]; + [pref_keys release]; + [pref_ctls release]; +# ifdef USE_PICKER_VIEW + [picker_values release]; +# endif +# endif [super dealloc]; }